diff --git "a/src/views/InstructorView.js" "b/src/views/InstructorView.js" --- "a/src/views/InstructorView.js" +++ "b/src/views/InstructorView.js" @@ -758,521 +758,486 @@ export function setupInstructorEvents() { } // Create Room - if (createBtn) { - // Dashboard Update Logic moved to top scope - - // Auto-Check Auth on Load - (async () => { - const { onAuthStateChanged } = await import("https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js"); - const { auth } = await import("../services/firebase.js"); - - onAuthStateChanged(auth, async (user) => { - if (user) { - // User is signed in, check if instructor permission - try { - const instructorData = await checkInstructorPermission(user); - if (instructorData) { - authModal.classList.add('hidden'); - checkPermissions(instructorData); - localStorage.setItem('vibecoding_instructor_name', instructorData.name); - - // Auto-reconnect room if exists in localStorage - const savedRoom = localStorage.getItem('vibecoding_room_code'); - if (savedRoom && !document.getElementById('dashboard-content').classList.contains('hidden')) { - // Already in dashboard, logic handled elsewhere or manual refresh needed - } else if (savedRoom) { - // Restore session logic if needed - // For now, let user click "Rejoin" or "Create". - // But wait, the user said "back button returns to login page". - // With this auth check, auth-modal will hide automatically. - } - } - } catch (e) { - console.error("Auth check failed:", e); - } - } - }); - })(); - // Create Room - if (createBtn) { - createBtn.addEventListener('click', async () => { - // 4-Digit Room Code - const roomCode = Math.floor(1000 + Math.random() * 9000).toString(); - try { - // Ensure roomInfo is visible - const roomInfo = document.getElementById('room-info'); - const displayRoomCode = document.getElementById('display-room-code'); - const createContainer = document.getElementById('create-room-container'); - const dashboardContent = document.getElementById('dashboard-content'); + // Create Room + if (createBtn) { + createBtn.addEventListener('click', async () => { + // 4-Digit Room Code + const roomCode = Math.floor(1000 + Math.random() * 9000).toString(); + try { + // Ensure roomInfo is visible + const roomInfo = document.getElementById('room-info'); + const displayRoomCode = document.getElementById('display-room-code'); + const createContainer = document.getElementById('create-room-container'); + const dashboardContent = document.getElementById('dashboard-content'); - await createRoom(roomCode, currentInstructor ? currentInstructor.name : 'Unknown'); + await createRoom(roomCode, currentInstructor ? currentInstructor.name : 'Unknown'); - // Trigger cleanup of old rooms - cleanupOldRooms(); + // Trigger cleanup of old rooms + cleanupOldRooms(); - displayRoomCode.textContent = roomCode; + displayRoomCode.textContent = roomCode; - // Store in LocalStorage - localStorage.setItem('vibecoding_room_code', roomCode); - localStorage.setItem('vibecoding_is_host', 'true'); + // Store in LocalStorage + localStorage.setItem('vibecoding_room_code', roomCode); + localStorage.setItem('vibecoding_is_host', 'true'); - // UI Updates - createContainer.classList.add('hidden'); - roomInfo.classList.remove('hidden'); - dashboardContent.classList.remove('hidden'); + // UI Updates + createContainer.classList.add('hidden'); + roomInfo.classList.remove('hidden'); + dashboardContent.classList.remove('hidden'); - // Start Subscription - subscribeToRoom(roomCode, (data) => { - updateDashboard(data); - }); + // Start Subscription + subscribeToRoom(roomCode, (data) => { + updateDashboard(data); + }); - } catch (e) { - console.error(e); - alert("無法建立教室: " + e.message); - } - }); - } + } catch (e) { + console.error(e); + alert("無法建立教室: " + e.message); + } + }); + } - // AI Settings Logic - if (setupAiBtn) { - setupAiBtn.addEventListener('click', () => { - const key = localStorage.getItem('vibecoding_gemini_key') || ''; - const studentEnabled = localStorage.getItem('vibecoding_student_ai_enabled') === 'true'; + // AI Settings Logic + if (setupAiBtn) { + setupAiBtn.addEventListener('click', () => { + const key = localStorage.getItem('vibecoding_gemini_key') || ''; + const studentEnabled = localStorage.getItem('vibecoding_student_ai_enabled') === 'true'; - if (geminiKeyInput) geminiKeyInput.value = key; - if (toggleStudentAi) toggleStudentAi.checked = studentEnabled; + if (geminiKeyInput) geminiKeyInput.value = key; + if (toggleStudentAi) toggleStudentAi.checked = studentEnabled; - if (aiSettingsModal) aiSettingsModal.classList.remove('hidden'); - }); - } + if (aiSettingsModal) aiSettingsModal.classList.remove('hidden'); + }); + } - if (saveAiSettingsBtn) { - saveAiSettingsBtn.addEventListener('click', async () => { - const key = geminiKeyInput ? geminiKeyInput.value.trim() : ''; - const studentEnabled = toggleStudentAi ? toggleStudentAi.checked : false; + if (saveAiSettingsBtn) { + saveAiSettingsBtn.addEventListener('click', async () => { + const key = geminiKeyInput ? geminiKeyInput.value.trim() : ''; + const studentEnabled = toggleStudentAi ? toggleStudentAi.checked : false; - if (key) { - localStorage.setItem('vibecoding_gemini_key', key); - } else { - localStorage.removeItem('vibecoding_gemini_key'); - } + if (key) { + localStorage.setItem('vibecoding_gemini_key', key); + } else { + localStorage.removeItem('vibecoding_gemini_key'); + } - localStorage.setItem('vibecoding_student_ai_enabled', studentEnabled); + localStorage.setItem('vibecoding_student_ai_enabled', studentEnabled); - // Update Firestore if in a room - const roomCode = localStorage.getItem('vibecoding_instructor_room'); + // Update Firestore if in a room + const roomCode = localStorage.getItem('vibecoding_instructor_room'); - if (roomCode) { - try { - const { getFirestore, doc, updateDoc } = await import("https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js"); - const db = getFirestore(); - await updateDoc(doc(db, "classrooms", roomCode), { ai_active: studentEnabled }); - } catch (e) { - console.error("Failed to sync AI setting to room:", e); - } + if (roomCode) { + try { + const { getFirestore, doc, updateDoc } = await import("https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js"); + const db = getFirestore(); + await updateDoc(doc(db, "classrooms", roomCode), { ai_active: studentEnabled }); + } catch (e) { + console.error("Failed to sync AI setting to room:", e); } + } - // Initialize Gemini (if key exists) - if (key) { - try { - const { initGemini } = await import("../services/gemini.js"); - await initGemini(key); - geminiEnabled = true; // Update module-level var - alert("AI 設定已儲存並啟動!"); - } catch (e) { - console.error(e); - alert("AI 啟動失敗: " + e.message); - } - } else { - geminiEnabled = false; - alert("AI 設定已儲存 (停用)"); + // Initialize Gemini (if key exists) + if (key) { + try { + const { initGemini } = await import("../services/gemini.js"); + await initGemini(key); + geminiEnabled = true; // Update module-level var + alert("AI 設定已儲存並啟動!"); + } catch (e) { + console.error(e); + alert("AI 啟動失敗: " + e.message); } + } else { + geminiEnabled = false; + alert("AI 設定已儲存 (停用)"); + } - if (aiSettingsModal) aiSettingsModal.classList.add('hidden'); - }); - } + if (aiSettingsModal) aiSettingsModal.classList.add('hidden'); + }); + } - // Rejoin Room - const rejoinBtn = document.getElementById('rejoin-room-btn'); - if (rejoinBtn) { - rejoinBtn.addEventListener('click', async () => { - const inputCode = document.getElementById('rejoin-room-code').value.trim().toUpperCase(); - if (!inputCode) return alert("請輸入代碼"); + // Rejoin Room + const rejoinBtn = document.getElementById('rejoin-room-btn'); + if (rejoinBtn) { + rejoinBtn.addEventListener('click', async () => { + const inputCode = document.getElementById('rejoin-room-code').value.trim().toUpperCase(); + if (!inputCode) return alert("請輸入代碼"); - try { - // Ensure roomInfo is visible - const roomInfo = document.getElementById('room-info'); - const displayRoomCode = document.getElementById('display-room-code'); - const createContainer = document.getElementById('create-room-container'); - const dashboardContent = document.getElementById('dashboard-content'); - - // Check if room exists first (optional, subscribe handles it usually) - displayRoomCode.textContent = inputCode; - localStorage.setItem('vibecoding_room_code', inputCode); - - // UI Updates - createContainer.classList.add('hidden'); - roomInfo.classList.remove('hidden'); - dashboardContent.classList.remove('hidden'); - - subscribeToRoom(inputCode, async (data) => { - // Check if updateDashboard is defined in scope - // Auto-Restore Room View if exists - if (localStorage.getItem('vibecoding_gemini_key')) { - const GeminiService = await import("../services/gemini.js?t=" + Date.now()); - const apiKey = localStorage.getItem('vibecoding_gemini_key'); - const success = await GeminiService.initGemini(apiKey); - if (success) { - geminiEnabled = true; - // Check student toggle - const studentEnabled = localStorage.getItem('vibecoding_student_ai_enabled') === 'true'; - // We need to store this in Firestore for students to see? - // Yes, 'settings/global' or room specific. - // For simplicity, let's assume 'settings/instructor_config' or just trust this is Local-only "AI Analysis" tool if not persisted. - // Wait, user wants Student AI to be enabled by this toggle. - // So we MUST persist this state to Firestore so StudentView can read it. - if (studentEnabled) { - // Update Firestore (assuming we have a 'settings' doc or room doc) - // Ideally we update the ROOM doc to say 'ai_enabled: true'. - // Finding current room... - const code = localStorage.getItem('vibecoding_room_code'); - if (code) { - const { updateDoc, doc, db } = await import("../services/classroom.js"); // We need raw Firestore access or a helper - // Let's simplify: Helper in classroom.js "setRoomSettings(code, { aiEnabled: true })" - // I'll add the Firestore update logic directly here for speed. - import("https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js").then(({ doc, updateDoc, getFirestore }) => { - const db = getFirestore(); - updateDoc(doc(db, "classrooms", code), { ai_active: true }).catch(console.error); - }); - } + try { + // Ensure roomInfo is visible + const roomInfo = document.getElementById('room-info'); + const displayRoomCode = document.getElementById('display-room-code'); + const createContainer = document.getElementById('create-room-container'); + const dashboardContent = document.getElementById('dashboard-content'); + + // Check if room exists first (optional, subscribe handles it usually) + displayRoomCode.textContent = inputCode; + localStorage.setItem('vibecoding_room_code', inputCode); + + // UI Updates + createContainer.classList.add('hidden'); + roomInfo.classList.remove('hidden'); + dashboardContent.classList.remove('hidden'); + + subscribeToRoom(inputCode, async (data) => { + // Check if updateDashboard is defined in scope + // Auto-Restore Room View if exists + if (localStorage.getItem('vibecoding_gemini_key')) { + const GeminiService = await import("../services/gemini.js?t=" + Date.now()); + const apiKey = localStorage.getItem('vibecoding_gemini_key'); + const success = await GeminiService.initGemini(apiKey); + if (success) { + geminiEnabled = true; + // Check student toggle + const studentEnabled = localStorage.getItem('vibecoding_student_ai_enabled') === 'true'; + // We need to store this in Firestore for students to see? + // Yes, 'settings/global' or room specific. + // For simplicity, let's assume 'settings/instructor_config' or just trust this is Local-only "AI Analysis" tool if not persisted. + // Wait, user wants Student AI to be enabled by this toggle. + // So we MUST persist this state to Firestore so StudentView can read it. + if (studentEnabled) { + // Update Firestore (assuming we have a 'settings' doc or room doc) + // Ideally we update the ROOM doc to say 'ai_enabled: true'. + // Finding current room... + const code = localStorage.getItem('vibecoding_room_code'); + if (code) { + const { updateDoc, doc, db } = await import("../services/classroom.js"); // We need raw Firestore access or a helper + // Let's simplify: Helper in classroom.js "setRoomSettings(code, { aiEnabled: true })" + // I'll add the Firestore update logic directly here for speed. + import("https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js").then(({ doc, updateDoc, getFirestore }) => { + const db = getFirestore(); + updateDoc(doc(db, "classrooms", code), { ai_active: true }).catch(console.error); + }); } } } + } - // Leave Room - const leaveBtn = document.getElementById('leave-room-btn'); - if (leaveBtn) { - leaveBtn.addEventListener('click', () => { - const roomInfo = document.getElementById('room-info'); - const createContainer = document.getElementById('create-room-container'); - const dashboardContent = document.getElementById('dashboard-content'); - const displayRoomCode = document.getElementById('display-room-code'); - - localStorage.removeItem('vibecoding_room_code'); - localStorage.removeItem('vibecoding_is_host'); - - displayRoomCode.textContent = ''; - roomInfo.classList.add('hidden'); - dashboardContent.classList.add('hidden'); - createContainer.classList.remove('hidden'); - - // Unsubscribe logic if needed, usually auto-handled by new subscription or page reload - window.location.reload(); - }); - } + // Leave Room + const leaveBtn = document.getElementById('leave-room-btn'); + if (leaveBtn) { + leaveBtn.addEventListener('click', () => { + const roomInfo = document.getElementById('room-info'); + const createContainer = document.getElementById('create-room-container'); + const dashboardContent = document.getElementById('dashboard-content'); + const displayRoomCode = document.getElementById('display-room-code'); - // Nav to Admin - if (navAdminBtn) { - navAdminBtn.addEventListener('click', () => { - localStorage.setItem('vibecoding_admin_referer', 'instructor'); - window.location.hash = '#admin'; - }); - } + localStorage.removeItem('vibecoding_room_code'); + localStorage.removeItem('vibecoding_is_host'); + + displayRoomCode.textContent = ''; + roomInfo.classList.add('hidden'); + dashboardContent.classList.add('hidden'); + createContainer.classList.remove('hidden'); - // Handle Instructor Management - navInstBtn.addEventListener('click', async () => { - const modal = document.getElementById('instructor-modal'); - const listBody = document.getElementById('instructor-list-body'); + // Unsubscribe logic if needed, usually auto-handled by new subscription or page reload + window.location.reload(); + }); + } - // Load list - const instructors = await getInstructors(); - listBody.innerHTML = instructors.map(inst => ` + // Nav to Admin + if (navAdminBtn) { + navAdminBtn.addEventListener('click', () => { + localStorage.setItem('vibecoding_admin_referer', 'instructor'); + window.location.hash = '#admin'; + }); + } + + // Handle Instructor Management + navInstBtn.addEventListener('click', async () => { + const modal = document.getElementById('instructor-modal'); + const listBody = document.getElementById('instructor-list-body'); + + // Load list + const instructors = await getInstructors(); + listBody.innerHTML = instructors.map(inst => `
${item.title}
@@ -2110,222 +2075,222 @@ export function setupInstructorEvents() { `; - grid.appendChild(col); - - // If blurred, remove blur after animation purely for effect, or keep? - // User intention "Hidden Name" usually means "Replaced by generic name". - // The blur effect in toggle logic was transient. - // If we want persistent anonymity, just "學員" is enough. - // The existing toggle logic adds 'blur-sm' then removes it in 300ms. - // We should replicate that effect if we want consistency, or just skip blur on init. - if (isAnonymous) { - const el = col.querySelector('.comparison-author'); - setTimeout(() => el.classList.remove('blur-sm'), 300); - } - }); - - document.getElementById('prompt-list-modal').classList.add('hidden'); - modal.classList.remove('hidden'); + grid.appendChild(col); + + // If blurred, remove blur after animation purely for effect, or keep? + // User intention "Hidden Name" usually means "Replaced by generic name". + // The blur effect in toggle logic was transient. + // If we want persistent anonymity, just "學員" is enough. + // The existing toggle logic adds 'blur-sm' then removes it in 300ms. + // We should replicate that effect if we want consistency, or just skip blur on init. + if (isAnonymous) { + const el = col.querySelector('.comparison-author'); + setTimeout(() => el.classList.remove('blur-sm'), 300); + } + }); - // Init Canvas (Phase 3) - setTimeout(setupCanvas, 100); + document.getElementById('prompt-list-modal').classList.add('hidden'); + modal.classList.remove('hidden'); + + // Init Canvas (Phase 3) + setTimeout(setupCanvas, 100); + }; + + window.closeComparison = () => { + document.getElementById('comparison-modal').classList.add('hidden'); + clearCanvas(); + }; + + // --- Phase 3 & 6: Annotation Tools --- + let canvas, ctx; + let isDrawing = false; + let currentPenColor = '#ef4444'; // Red default + let currentLineWidth = 3; + let currentMode = 'source-over'; // 'source-over' (Pen) or 'destination-out' (Eraser) + + window.setupCanvas = () => { + canvas = document.getElementById('annotation-canvas'); + const container = document.getElementById('comparison-container'); + if (!canvas || !container) return; + + ctx = canvas.getContext('2d'); + + // Resize + const resize = () => { + canvas.width = container.clientWidth; + canvas.height = container.clientHeight; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = currentPenColor; + ctx.lineWidth = currentLineWidth; + ctx.globalCompositeOperation = currentMode; }; + resize(); + window.addEventListener('resize', resize); + + // Init Size UI & Cursor + updateSizeBtnUI(); + updateCursorStyle(); + + // Cursor Logic + const cursor = document.getElementById('tool-cursor'); + + canvas.addEventListener('mouseenter', () => cursor.classList.remove('hidden')); + canvas.addEventListener('mouseleave', () => cursor.classList.add('hidden')); + canvas.addEventListener('mousemove', (e) => { + const { x, y } = getPos(e); + cursor.style.left = `${x}px`; + cursor.style.top = `${y}px`; + }); - window.closeComparison = () => { - document.getElementById('comparison-modal').classList.add('hidden'); - clearCanvas(); - }; + // Drawing Events + const start = (e) => { + isDrawing = true; + ctx.beginPath(); - // --- Phase 3 & 6: Annotation Tools --- - let canvas, ctx; - let isDrawing = false; - let currentPenColor = '#ef4444'; // Red default - let currentLineWidth = 3; - let currentMode = 'source-over'; // 'source-over' (Pen) or 'destination-out' (Eraser) - - window.setupCanvas = () => { - canvas = document.getElementById('annotation-canvas'); - const container = document.getElementById('comparison-container'); - if (!canvas || !container) return; - - ctx = canvas.getContext('2d'); - - // Resize - const resize = () => { - canvas.width = container.clientWidth; - canvas.height = container.clientHeight; - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - ctx.strokeStyle = currentPenColor; - ctx.lineWidth = currentLineWidth; - ctx.globalCompositeOperation = currentMode; - }; - resize(); - window.addEventListener('resize', resize); - - // Init Size UI & Cursor - updateSizeBtnUI(); - updateCursorStyle(); - - // Cursor Logic - const cursor = document.getElementById('tool-cursor'); - - canvas.addEventListener('mouseenter', () => cursor.classList.remove('hidden')); - canvas.addEventListener('mouseleave', () => cursor.classList.add('hidden')); - canvas.addEventListener('mousemove', (e) => { - const { x, y } = getPos(e); - cursor.style.left = `${x}px`; - cursor.style.top = `${y}px`; - }); + // Re-apply settings (state might change) + ctx.globalCompositeOperation = currentMode; + ctx.strokeStyle = currentPenColor; + ctx.lineWidth = currentLineWidth; - // Drawing Events - const start = (e) => { - isDrawing = true; - ctx.beginPath(); - - // Re-apply settings (state might change) - ctx.globalCompositeOperation = currentMode; - ctx.strokeStyle = currentPenColor; - ctx.lineWidth = currentLineWidth; - - const { x, y } = getPos(e); - ctx.moveTo(x, y); - }; - - const move = (e) => { - if (!isDrawing) return; - const { x, y } = getPos(e); - ctx.lineTo(x, y); - ctx.stroke(); - }; - - const end = () => { - isDrawing = false; - }; - - canvas.onmousedown = start; - canvas.onmousemove = move; - canvas.onmouseup = end; - canvas.onmouseleave = end; - - // Touch support - canvas.ontouchstart = (e) => { e.preventDefault(); start(e.touches[0]); }; - canvas.ontouchmove = (e) => { e.preventDefault(); move(e.touches[0]); }; - canvas.ontouchend = (e) => { e.preventDefault(); end(); }; + const { x, y } = getPos(e); + ctx.moveTo(x, y); }; - function getPos(e) { - const rect = canvas.getBoundingClientRect(); - return { - x: e.clientX - rect.left, - y: e.clientY - rect.top - }; - } - - // Unified Tool Handler - window.setPenTool = (tool, color, btn) => { - // UI Update - document.querySelectorAll('.annotation-tool').forEach(b => { - b.classList.remove('ring-white'); - b.classList.add('ring-transparent'); - }); - btn.classList.remove('ring-transparent'); - btn.classList.add('ring-white'); - - if (tool === 'eraser') { - currentMode = 'destination-out'; - // Force larger eraser size (e.g., 3x current size or fixed large) - // We'll multiply current selected size by 4 for better UX - const multiplier = 4; - // Store original explicitly if needed, but currentLineWidth is global. - // We should dynamically adjust context lineWidth during draw, or just hack it here. - // Hack: If we change currentLineWidth here, the UI size buttons might look wrong. - // Better: Update cursor style only? No, actual draw needs it. - // Let's set a separate 'actualWidth' used in draw, OR just change currentLineWidth temporarily? - // Simpler: Just change it. When user clicks size button, it resets. - // But if user clicks Pen back? We need to restore. - // Let's rely on setPenTool being called with color. - // When "Pen" is clicked, we usually don't call setPenTool with a saved size... - // Actually, let's just use a large default for eraser, and keep currentLineWidth for Pen. - // We need to change how draw() uses the width. - // BUT, since we don't want to touch draw() deep inside: - // We will hijack currentLineWidth. - if (!window.savedPenWidth) window.savedPenWidth = currentLineWidth; - currentLineWidth = window.savedPenWidth * 4; - } else { - currentMode = 'source-over'; - currentPenColor = color; - // Restore pen width - if (window.savedPenWidth) { - currentLineWidth = window.savedPenWidth; - window.savedPenWidth = null; - } - } - updateCursorStyle(); + const move = (e) => { + if (!isDrawing) return; + const { x, y } = getPos(e); + ctx.lineTo(x, y); + ctx.stroke(); }; - // Size Handler - window.setPenSize = (size, btn) => { - currentLineWidth = size; - updateSizeBtnUI(); - updateCursorStyle(); + const end = () => { + isDrawing = false; }; - function updateCursorStyle() { - const cursor = document.getElementById('tool-cursor'); - if (!cursor) return; - - // Size - cursor.style.width = `${currentLineWidth}px`; - cursor.style.height = `${currentLineWidth}px`; + canvas.onmousedown = start; + canvas.onmousemove = move; + canvas.onmouseup = end; + canvas.onmouseleave = end; + + // Touch support + canvas.ontouchstart = (e) => { e.preventDefault(); start(e.touches[0]); }; + canvas.ontouchmove = (e) => { e.preventDefault(); move(e.touches[0]); }; + canvas.ontouchend = (e) => { e.preventDefault(); end(); }; + }; + + function getPos(e) { + const rect = canvas.getBoundingClientRect(); + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; + } - // Color - if (currentMode === 'destination-out') { - // Eraser: White solid - cursor.style.backgroundColor = 'white'; - cursor.style.borderColor = '#999'; - } else { - // Pen: Tool color - cursor.style.backgroundColor = currentPenColor; - cursor.style.borderColor = 'rgba(255,255,255,0.8)'; + // Unified Tool Handler + window.setPenTool = (tool, color, btn) => { + // UI Update + document.querySelectorAll('.annotation-tool').forEach(b => { + b.classList.remove('ring-white'); + b.classList.add('ring-transparent'); + }); + btn.classList.remove('ring-transparent'); + btn.classList.add('ring-white'); + + if (tool === 'eraser') { + currentMode = 'destination-out'; + // Force larger eraser size (e.g., 3x current size or fixed large) + // We'll multiply current selected size by 4 for better UX + const multiplier = 4; + // Store original explicitly if needed, but currentLineWidth is global. + // We should dynamically adjust context lineWidth during draw, or just hack it here. + // Hack: If we change currentLineWidth here, the UI size buttons might look wrong. + // Better: Update cursor style only? No, actual draw needs it. + // Let's set a separate 'actualWidth' used in draw, OR just change currentLineWidth temporarily? + // Simpler: Just change it. When user clicks size button, it resets. + // But if user clicks Pen back? We need to restore. + // Let's rely on setPenTool being called with color. + // When "Pen" is clicked, we usually don't call setPenTool with a saved size... + // Actually, let's just use a large default for eraser, and keep currentLineWidth for Pen. + // We need to change how draw() uses the width. + // BUT, since we don't want to touch draw() deep inside: + // We will hijack currentLineWidth. + if (!window.savedPenWidth) window.savedPenWidth = currentLineWidth; + currentLineWidth = window.savedPenWidth * 4; + } else { + currentMode = 'source-over'; + currentPenColor = color; + // Restore pen width + if (window.savedPenWidth) { + currentLineWidth = window.savedPenWidth; + window.savedPenWidth = null; } } - - function updateSizeBtnUI() { - document.querySelectorAll('.size-btn').forEach(b => { - if (parseInt(b.dataset.size) === currentLineWidth) { - b.classList.add('bg-gray-600', 'text-white'); - b.classList.remove('text-gray-400', 'hover:bg-gray-700'); - } else { - b.classList.remove('bg-gray-600', 'text-white'); - b.classList.add('text-gray-400', 'hover:bg-gray-700'); - } - }); + updateCursorStyle(); + }; + + // Size Handler + window.setPenSize = (size, btn) => { + currentLineWidth = size; + updateSizeBtnUI(); + updateCursorStyle(); + }; + + function updateCursorStyle() { + const cursor = document.getElementById('tool-cursor'); + if (!cursor) return; + + // Size + cursor.style.width = `${currentLineWidth}px`; + cursor.style.height = `${currentLineWidth}px`; + + // Color + if (currentMode === 'destination-out') { + // Eraser: White solid + cursor.style.backgroundColor = 'white'; + cursor.style.borderColor = '#999'; + } else { + // Pen: Tool color + cursor.style.backgroundColor = currentPenColor; + cursor.style.borderColor = 'rgba(255,255,255,0.8)'; } + } - window.clearCanvas = () => { - if (canvas && ctx) { - ctx.clearRect(0, 0, canvas.width, canvas.height); + function updateSizeBtnUI() { + document.querySelectorAll('.size-btn').forEach(b => { + if (parseInt(b.dataset.size) === currentLineWidth) { + b.classList.add('bg-gray-600', 'text-white'); + b.classList.remove('text-gray-400', 'hover:bg-gray-700'); + } else { + b.classList.remove('bg-gray-600', 'text-white'); + b.classList.add('text-gray-400', 'hover:bg-gray-700'); } - }; - - /** - * Renders the Transposed Heatmap (Rows=Challenges, Cols=Students) - */ - function renderTransposedHeatmap(students) { - const thead = document.getElementById('heatmap-header'); - const tbody = document.getElementById('heatmap-body'); + }); + } - if (students.length === 0) { - thead.innerHTML = '