File size: 19,564 Bytes
21778f6
66269b4
 
 
 
 
d874f33
 
 
 
 
901982e
 
66269b4
 
 
 
 
 
 
 
d874f33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21778f6
 
 
d874f33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
901982e
d874f33
 
 
 
 
 
 
 
 
901982e
d874f33
41fc024
d874f33
 
 
 
 
 
 
684f9a1
d874f33
901982e
8c465da
d874f33
 
 
684f9a1
901982e
 
 
d874f33
 
 
 
 
 
 
 
 
 
 
 
 
 
901982e
 
 
 
 
 
66269b4
 
 
 
 
d874f33
051bc5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66269b4
d874f33
 
 
 
 
 
66269b4
901982e
 
 
 
 
684f9a1
 
 
d874f33
 
 
684f9a1
 
901982e
d874f33
8c465da
 
 
 
 
 
 
 
 
 
 
 
901982e
 
 
d874f33
901982e
8c465da
901982e
 
d874f33
901982e
 
 
8c465da
 
 
 
 
 
 
 
b391d41
8c465da
b391d41
8c465da
 
d874f33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21778f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d874f33
 
b391d41
 
 
 
 
 
d874f33
b391d41
 
 
 
 
d874f33
b391d41
 
 
 
 
 
 
 
d874f33
b391d41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d874f33
 
b391d41
901982e
b391d41
 
 
901982e
 
 
b391d41
 
 
66269b4
b391d41
 
d874f33
b391d41
d874f33
 
66269b4
d874f33
 
b391d41
d874f33
 
21778f6
d874f33
 
b391d41
d874f33
 
66269b4
d874f33
b391d41
d874f33
 
b391d41
d874f33
 
 
 
901982e
 
b391d41
 
d874f33
 
 
901982e
 
 
b391d41
901982e
b391d41
 
 
 
 
 
 
 
 
901982e
d874f33
b391d41
d874f33
901982e
 
 
d874f33
 
21778f6
d874f33
 
 
 
 
 
 
 
21778f6
 
 
 
d874f33
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
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 `

    <div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur z-50 flex items-center justify-center">

        <div class="bg-gray-800 p-8 rounded-xl border border-gray-600 shadow-2xl max-w-sm w-full">

            <h2 class="text-xl font-bold text-center mb-6 text-white">🔒 講師身分驗證</h2>

            <input type="password" id="instructor-password" class="w-full bg-gray-900 border border-gray-700 rounded p-3 text-white text-center text-lg tracking-widest mb-4 focus:border-cyan-500 focus:outline-none" placeholder="輸入密碼">

            <button id="auth-btn" class="w-full bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 rounded-lg transition-colors">確認進入</button>

        </div>

    </div>



    <!-- Broadcast Modal (Hidden by default) -->

    <div id="broadcast-modal" class="fixed inset-0 bg-black/90 backdrop-blur z-50 hidden flex flex-col items-center justify-center p-8 transition-opacity duration-300">

        <button onclick="closeBroadcast()" class="absolute top-6 right-6 text-gray-400 hover:text-white text-2xl">✕</button>

        

        <div id="broadcast-content" class="bg-gray-800 border border-gray-600 rounded-2xl p-8 max-w-4xl w-full text-center shadow-2xl transform transition-transform scale-95 opacity-0">

            <div class="mb-4 flex flex-col items-center">

                <div class="w-16 h-16 rounded-full bg-cyan-600 flex items-center justify-center text-3xl font-bold text-white mb-2" id="broadcast-avatar">

                    D

                </div>

                <h3 class="text-xl text-cyan-300 font-bold" id="broadcast-author">Dave</h3>

                <span class="text-gray-500 text-sm" id="broadcast-challenge">Challenge Name</span>

            </div>

            

            <div class="bg-black/30 rounded-xl p-6 mb-8 text-left overflow-auto max-h-[50vh]">

                <pre class="text-green-400 font-mono text-lg whitespace-pre-wrap" id="broadcast-prompt">Loading...</pre>

            </div>



            <div class="flex justify-center space-x-4">

               <button id="btn-show-stage" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg">

                    <span>🖥️ 投放到大螢幕 (本機)</span>

               </button>

               <button id="btn-reject-task" class="bg-red-600 hover:bg-red-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg shadow-lg">

                    <span>🛑 退回重做 (Reject)</span>

               </button>

               <!-- Future Feature: Send to Students -->

               <!-- 

               <button id="btn-broadcast-all" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-3 px-8 rounded-xl flex items-center space-x-2 text-lg opacity-50 cursor-not-allowed" title="此功能開發中">

                    <span>📡 推送給所有人</span>

               </button>

               -->

            </div>

        </div>

        

        <!-- Big Screen Mode (Initially hidden inside modal) -->

        <div id="stage-view" class="hidden absolute inset-0 bg-gray-900 flex flex-col items-center justify-center p-10">

             <button onclick="closeStage()" class="absolute top-6 right-6 text-gray-500 hover:text-white text-4xl">✕</button>

             <h1 class="text-4xl font-bold text-cyan-400 mb-8" id="stage-title">優秀作品展示</h1>

             <div class="bg-black border-2 border-cyan-500/50 rounded-2xl p-10 max-w-6xl w-full shadow-[0_0_50px_rgba(6,182,212,0.2)]">

                <pre class="text-3xl text-green-400 font-mono whitespace-pre-wrap leading-relaxed" id="stage-prompt">...</pre>

             </div>

             <div class="mt-8 text-2xl text-gray-400">

                Author: <span class="text-white font-bold" id="stage-author">Dave</span>

             </div>

        </div>

    </div>



    <div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">

        <!-- Header -->

        <header class="flex flex-col md:flex-row justify-between items-center mb-6 bg-gray-800 p-4 rounded-xl border border-gray-700 space-y-4 md:space-y-0 sticky top-0 z-30 shadow-lg">

            <div class="flex items-center space-x-4">

                <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600">

                    儀表板

                </h1>

                <div id="room-info" class="hidden flex items-center space-x-2 bg-black/30 px-3 py-1 rounded-lg border border-gray-700">

                    <span class="text-xs text-gray-500 uppercase">Room</span>

                    <span id="display-room-code" class="text-xl font-mono font-bold text-cyan-400 tracking-widest"></span>

                </div>

            </div>

            

            <div class="flex space-x-3">

                <div class="flex items-center space-x-2 text-xs text-gray-400 mr-4 border-r border-gray-700 pr-4">

                    <div class="flex items-center"><div class="w-3 h-3 bg-gray-700 rounded-sm mr-1"></div> 未開始</div>

                    <div class="flex items-center"><div class="w-3 h-3 bg-blue-600 rounded-sm mr-1"></div> 進行中</div>

                    <div class="flex items-center"><div class="w-3 h-3 bg-green-500 rounded-sm mr-1"></div> 已完成</div>

                    <div class="flex items-center"><div class="w-3 h-3 bg-red-500 animate-pulse rounded-sm mr-1"></div> 卡關 (>5m)</div>

                </div>



                <button id="nav-admin-btn" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition-all border border-gray-600">

                    管理題目

                </button>

                <div id="create-room-container" class="flex items-center space-x-2">

                    <input type="text" id="rejoin-room-code" placeholder="代碼" class="bg-gray-900 border border-gray-700 text-white px-3 py-2 rounded-lg w-20 text-center focus:outline-none focus:border-cyan-500">

                    <button id="rejoin-room-btn" class="bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded-lg">重回</button>

                    <button id="create-room-btn" class="bg-purple-600 hover:bg-purple-500 text-white font-bold px-4 py-2 rounded-lg shadow-lg">開房</button>

                </div>

            </div>

        </header>



        <!-- Heatmap Content -->

        <div id="dashboard-content" class="hidden overflow-x-auto pb-10">

            <table class="w-full border-collapse">

                <thead>

                    <tr id="heatmap-header">

                        <th class="p-3 text-left sticky left-0 bg-gray-800 z-20 border-b border-gray-700 min-w-[150px]">學員 / 關卡</th>

                        <!-- Challenges headers generated dynamically -->

                    </tr>

                </thead>

                <tbody id="heatmap-body">

                    <!-- Rows generated dynamically -->

                    <tr><td colspan="100" class="text-center py-10 text-gray-500">等待資料載入...</td></tr>

                </tbody>

            </table>

        </div>

    </div>

    `;
}

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 = '<th class="p-4 text-left">等待資料...</th>';
        tbody.innerHTML = '<tr><td class="p-10 text-center text-gray-500">尚無學員加入</td></tr>';
        return;
    }

    // 1. Render Header (Students)
    // Sticky Top for Header Row
    // Sticky Left for the first cell ("Challenge/Student")
    let headerHtml = `

        <th class="p-3 text-left sticky left-0 top-0 bg-gray-800 z-30 border-b border-gray-600 min-w-[200px] border-r border-gray-700 shadow-md">

            <div class="flex justify-between items-end">

                <span class="text-sm text-gray-400">題目</span>

                <span class="text-sm text-cyan-400">學員 (${students.length})</span>

            </div>

        </th>

    `;

    students.forEach(student => {
        headerHtml += `

            <th class="p-2 text-center sticky top-0 bg-gray-800 z-20 border-b border-gray-700 min-w-[80px] group">

                <div class="flex flex-col items-center space-y-2 py-2">

                    <div class="w-8 h-8 rounded-full bg-gradient-to-br from-gray-700 to-gray-600 flex items-center justify-center text-xs font-bold text-white uppercase border border-gray-500 shadow-sm relative">

                        ${student.nickname[0]}

                        <!-- Online Indicator (Simulated) -->

                        <div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>

                    </div>

                    <span class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr" style="writing-mode: vertical-rl; text-orientation: mixed;">

                        ${student.nickname}

                    </span>

                </div>

            </th>

        `;
    });
    thead.innerHTML = headerHtml;

    // 2. Render Body (Challenges as Rows)
    if (cachedChallenges.length === 0) {
        tbody.innerHTML = '<tr><td colspan="100" class="text-center py-4">沒有題目資料</td></tr>';
        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, '&quot;').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 `

                <td class="p-1 border border-gray-800/50 text-center align-middle h-14 hover:bg-white/5 transition-colors">

                    <div class="mx-auto w-10 h-10 rounded-lg border flex items-center justify-center ${statusClass} transition-all duration-300" ${action}>

                        ${content}

                    </div>

                </td>

            `;
        }).join('');

        // Row Header (Challenge Title)
        return `

            <tr class="hover:bg-gray-800/50 transition-colors">

                <td class="p-3 sticky left-0 bg-gray-900 z-10 border-r border-b border-gray-700 shadow-md">

                    <div class="flex items-center justify-between">

                       <div class="flex flex-col">

                           <span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-0.5">${c.level}</span>

                           <span class="font-bold text-white text-sm truncate max-w-[180px]" title="${c.title}">${index + 1}. ${c.title}</span>

                       </div>

                       <!-- Stats (Optional) -->

                       <!-- <span class="text-xs text-gray-500">0%</span> -->

                    </div>

                </td>

                ${rowCells}

            </tr>

        `;
    }).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);
};