import { createRoom, subscribeToRoom, getChallenges, resetProgress } from "../services/classroom.js"; let cachedChallenges = []; export async function renderInstructorView() { // Pre-fetch challenges for table headers try { cachedChallenges = await getChallenges(); } catch (e) { console.error("Failed header load", e); } return `

🔒 講師身分驗證

儀表板

未開始
進行中
已完成
卡關 (>5m)
`; } export function setupInstructorEvents() { // Auth Logic const authBtn = document.getElementById('auth-btn'); const pwdInput = document.getElementById('instructor-password'); const authModal = document.getElementById('auth-modal'); // Default password check const checkPassword = async () => { const { verifyInstructorPassword } = await import("../services/classroom.js"); authBtn.textContent = "驗證中..."; authBtn.disabled = true; try { const isValid = await verifyInstructorPassword(pwdInput.value); if (isValid) { authModal.classList.add('hidden'); // Store session to avoid re-login on reload (Optional, for now just per session) } else { alert('密碼錯誤'); pwdInput.value = ''; } } catch (e) { console.error(e); alert("驗證出錯"); } finally { authBtn.textContent = "確認進入"; authBtn.disabled = false; } }; authBtn.addEventListener('click', checkPassword); pwdInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') checkPassword(); }); const createBtn = document.getElementById('create-room-btn'); 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'); const navAdminBtn = document.getElementById('nav-admin-btn'); navAdminBtn.addEventListener('click', () => { // Save current room to return later const currentRoom = localStorage.getItem('vibecoding_instructor_room'); localStorage.setItem('vibecoding_admin_referer', 'instructor'); // track entry source window.location.hash = 'admin'; }); // Auto-fill code const savedRoomCode = localStorage.getItem('vibecoding_instructor_room'); if (savedRoomCode) { document.getElementById('rejoin-room-code').value = savedRoomCode; } const rejoinBtn = document.getElementById('rejoin-room-btn'); rejoinBtn.addEventListener('click', () => { const code = document.getElementById('rejoin-room-code').value.trim(); if (!code) return alert('請輸入教室代碼'); enterRoom(code); }); createBtn.addEventListener('click', async () => { try { createBtn.disabled = true; createBtn.textContent = "..."; const roomCode = await createRoom(); enterRoom(roomCode); } catch (error) { console.error(error); alert("建立失敗"); createBtn.disabled = false; } }); function enterRoom(roomCode) { createContainer.classList.add('hidden'); roomInfo.classList.remove('hidden'); dashboardContent.classList.remove('hidden'); displayRoomCode.textContent = roomCode; localStorage.setItem('vibecoding_instructor_room', roomCode); // Subscribe to updates subscribeToRoom(roomCode, (students) => { renderTransposedHeatmap(students); }); } // Modal Events window.closeBroadcast = () => { const modal = document.getElementById('broadcast-modal'); const content = document.getElementById('broadcast-content'); content.classList.remove('opacity-100', 'scale-100'); content.classList.add('scale-95', 'opacity-0'); setTimeout(() => modal.classList.add('hidden'), 300); }; window.openStage = (prompt, author) => { document.getElementById('broadcast-content').classList.add('hidden'); const stage = document.getElementById('stage-view'); stage.classList.remove('hidden'); document.getElementById('stage-prompt').textContent = prompt; document.getElementById('stage-author').textContent = author; }; window.closeStage = () => { document.getElementById('stage-view').classList.add('hidden'); document.getElementById('broadcast-content').classList.remove('hidden'); }; document.getElementById('btn-show-stage').addEventListener('click', () => { const prompt = document.getElementById('broadcast-prompt').textContent; const author = document.getElementById('broadcast-author').textContent; window.openStage(prompt, author); }); // Reject Logic document.getElementById('btn-reject-task').addEventListener('click', async () => { if (!confirm('確定要退回此題目讓學員重做嗎?')) return; // We need student ID (userId) and Challenge ID. // Currently showBroadcastModal only receives nickname, title, prompt. // We need to attach data-userid and data-challengeid to the modal. const modal = document.getElementById('broadcast-modal'); const userId = modal.dataset.userId; const challengeId = modal.dataset.challengeId; const roomCode = localStorage.getItem('vibecoding_instructor_room'); if (userId && challengeId && roomCode) { try { await resetProgress(userId, roomCode, challengeId); // Close modal window.closeBroadcast(); } catch (e) { console.error(e); alert('退回失敗'); } } }); } /** * 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 = '等待資料...'; tbody.innerHTML = '尚無學員加入'; return; } // 1. Render Header (Students) // Sticky Top for Header Row // Sticky Left for the first cell ("Challenge/Student") let headerHtml = `
題目 學員 (${students.length})
`; students.forEach(student => { headerHtml += `
${student.nickname[0]}
${student.nickname}
`; }); thead.innerHTML = headerHtml; // 2. Render Body (Challenges as Rows) if (cachedChallenges.length === 0) { tbody.innerHTML = '沒有題目資料'; return; } tbody.innerHTML = cachedChallenges.map((c, index) => { const colors = { beginner: 'cyan', intermediate: 'blue', advanced: 'purple' }; const color = colors[c.level] || 'gray'; // Build Row Cells (One per student) const rowCells = students.map(student => { const p = student.progress?.[c.id]; let statusClass = 'bg-gray-800/30 border-gray-800'; // Default let content = ''; let action = ''; if (p) { if (p.status === 'completed') { statusClass = 'bg-green-500/20 border-green-500/50 hover:bg-green-500/40 cursor-pointer shadow-[0_0_10px_rgba(34,197,94,0.1)]'; content = '✅'; const safePrompt = p.prompt.replace(/"/g, '"').replace(/'/g, "\\'"); action = `onclick="window.showBroadcastModal('${student.id}', '${c.id}', '${student.nickname}', '${c.title}', '${safePrompt}')"`; } else if (p.status === 'started') { // Check stuck const startedAt = p.timestamp ? p.timestamp.toDate() : new Date(); const now = new Date(); const diffMins = (now - startedAt) / 1000 / 60; if (diffMins > 5) { statusClass = 'bg-red-900/50 border-red-500 animate-pulse cursor-help'; content = '🆘'; } else { statusClass = 'bg-blue-600/20 border-blue-500'; content = '🔵'; } } } return `
${content}
`; }).join(''); // Row Header (Challenge Title) return `
${c.level} ${index + 1}. ${c.title}
${rowCells} `; }).join(''); } // Global scope for HTML access window.showBroadcastModal = (userId, challengeId, nickname, title, prompt) => { const modal = document.getElementById('broadcast-modal'); const content = document.getElementById('broadcast-content'); document.getElementById('broadcast-avatar').textContent = nickname[0]; document.getElementById('broadcast-author').textContent = nickname; document.getElementById('broadcast-challenge').textContent = title; document.getElementById('broadcast-prompt').textContent = prompt; // Store IDs for actions modal.dataset.userId = userId; modal.dataset.challengeId = challengeId; modal.classList.remove('hidden'); // Animation trigger setTimeout(() => { content.classList.remove('scale-95', 'opacity-0'); content.classList.add('opacity-100', 'scale-100'); }, 10); };