Lashtw commited on
Commit
901982e
·
verified ·
1 Parent(s): 986dc10

Upload 7 files

Browse files
index.html CHANGED
@@ -1,19 +1,34 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Vibecoding Workshop</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Noto+Sans+TC:wght@400;700&display=swap"
13
+ rel="stylesheet">
14
+ <style>
15
+ body {
16
+ font-family: 'Noto Sans TC', sans-serif;
17
+ background-color: #0f172a;
18
+ /* Slate 900 */
19
+ color: #e2e8f0;
20
+ /* Slate 200 */
21
+ }
22
+
23
+ .font-mono {
24
+ font-family: 'JetBrains Mono', monospace;
25
+ }
26
+ </style>
27
+ </head>
28
+
29
+ <body class="bg-slate-900 text-slate-200 antialiased selection:bg-cyan-500 selection:text-white">
30
+ <div id="app" class="max-w-7xl mx-auto"></div>
31
+ <script type="module" src="./src/main.js"></script>
32
+ </body>
33
+
34
+ </html>
src/main.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { renderLandingView, setupLandingEvents } from './views/LandingView.js';
2
+ import { renderInstructorView, setupInstructorEvents } from './views/InstructorView.js';
3
+ import { renderStudentView, setupStudentEvents } from './views/StudentView.js';
4
+
5
+ const app = document.querySelector('#app');
6
+
7
+ function navigateTo(view) {
8
+ switch (view) {
9
+ case 'landing':
10
+ app.innerHTML = renderLandingView();
11
+ setupLandingEvents(navigateTo);
12
+ break;
13
+ case 'instructor':
14
+ app.innerHTML = renderInstructorView();
15
+ setupInstructorEvents();
16
+ break;
17
+ case 'student':
18
+ app.innerHTML = renderStudentView();
19
+ setupStudentEvents();
20
+ break;
21
+ default:
22
+ app.innerHTML = renderLandingView();
23
+ setupLandingEvents(navigateTo);
24
+ }
25
+ }
26
+
27
+ // Initial Load
28
+ navigateTo('landing');
src/services/classroom.js ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { db } from "./firebase.js";
2
+ import {
3
+ collection,
4
+ doc,
5
+ setDoc,
6
+ getDoc,
7
+ addDoc,
8
+ onSnapshot,
9
+ serverTimestamp,
10
+ query,
11
+ where,
12
+ getDocs
13
+ } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
14
+
15
+ // Collection references
16
+ const ROOMS_COLLECTION = "classrooms";
17
+
18
+ /**
19
+ * Creates a new classroom room
20
+ * @returns {Promise<string>} The room code
21
+ */
22
+ export async function createRoom() {
23
+ const roomCode = Math.floor(1000 + Math.random() * 9000).toString(); // Simple 4-digit code
24
+ const roomRef = doc(db, ROOMS_COLLECTION, roomCode);
25
+
26
+ await setDoc(roomRef, {
27
+ createdAt: serverTimestamp(),
28
+ active: true
29
+ });
30
+
31
+ return roomCode;
32
+ }
33
+
34
+ /**
35
+ * Joins an existing room
36
+ * @param {string} roomCode
37
+ * @param {string} nickname
38
+ * @returns {Promise<string>} The student ID
39
+ */
40
+ export async function joinRoom(roomCode, nickname) {
41
+ const roomRef = doc(db, ROOMS_COLLECTION, roomCode);
42
+ const roomSnap = await getDoc(roomRef);
43
+
44
+ if (!roomSnap.exists()) {
45
+ throw new Error("教室代碼不存在");
46
+ }
47
+
48
+ // Create student document within the room
49
+ const studentsRef = collection(roomRef, "students");
50
+ const studentDoc = await addDoc(studentsRef, {
51
+ nickname,
52
+ joinedAt: serverTimestamp(),
53
+ progress: {}
54
+ });
55
+
56
+ return studentDoc.id;
57
+ }
58
+
59
+ /**
60
+ * Submits a prompt for a specific level
61
+ * @param {string} roomCode
62
+ * @param {string} studentId
63
+ * @param {string} levelId
64
+ * @param {string} prompt
65
+ */
66
+ export async function submitPrompt(roomCode, studentId, levelId, prompt) {
67
+ const text = prompt.trim();
68
+ if (!text) return;
69
+
70
+ const studentRef = doc(db, ROOMS_COLLECTION, roomCode, "students", studentId);
71
+
72
+ // Update using merge to preserve other progress
73
+ await setDoc(studentRef, {
74
+ progress: {
75
+ [levelId]: {
76
+ status: "completed",
77
+ prompt: text,
78
+ timestamp: serverTimestamp()
79
+ }
80
+ },
81
+ lastActive: serverTimestamp()
82
+ }, { merge: true });
83
+ }
84
+
85
+ /**
86
+ * Subscribes to room updates (for Instructor)
87
+ * @param {string} roomCode
88
+ * @param {Function} callback
89
+ * @returns {Function} Unsubscribe function
90
+ */
91
+ export function subscribeToRoom(roomCode, callback) {
92
+ const studentsRef = collection(db, ROOMS_COLLECTION, roomCode, "students");
93
+
94
+ return onSnapshot(studentsRef, (snapshot) => {
95
+ const students = [];
96
+ snapshot.forEach((doc) => {
97
+ students.push({ id: doc.id, ...doc.data() });
98
+ });
99
+ callback(students);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Fetches prompts for a specific level from all students in the room
105
+ * @param {string} roomCode
106
+ * @param {string} levelId
107
+ * @returns {Promise<Array>} Array of { nickname, prompt }
108
+ */
109
+ export async function getPeerPrompts(roomCode, levelId) {
110
+ const studentsRef = collection(db, ROOMS_COLLECTION, roomCode, "students");
111
+ const snapshot = await getDocs(studentsRef);
112
+
113
+ const prompts = [];
114
+ snapshot.forEach(doc => {
115
+ const data = doc.data();
116
+ const levelData = data.progress?.[levelId];
117
+ if (levelData && levelData.status === 'completed' && levelData.prompt) {
118
+ prompts.push({
119
+ nickname: data.nickname,
120
+ prompt: levelData.prompt,
121
+ timestamp: levelData.timestamp
122
+ });
123
+ }
124
+ });
125
+
126
+ return prompts;
127
+ }
src/services/firebase.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
2
+ import { getFirestore } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
3
+
4
+ const firebaseConfig = {
5
+ apiKey: "AIzaSyDq2a-yE6pbaeNf6KzkTcogh9oi-6nQbKk",
6
+ authDomain: "vibecodingex.firebaseapp.com",
7
+ projectId: "vibecodingex",
8
+ storageBucket: "vibecodingex.firebasestorage.app",
9
+ messagingSenderId: "831513336128",
10
+ appId: "1:831513336128:web:e31d2654fcfa19fb2642a5"
11
+ };
12
+
13
+ const app = initializeApp(firebaseConfig);
14
+ const db = getFirestore(app);
15
+
16
+ export { db };
src/views/InstructorView.js ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRoom, subscribeToRoom } from "../services/classroom.js";
2
+
3
+ export function renderInstructorView() {
4
+ return `
5
+ <div class="min-h-screen p-6">
6
+ <!-- Header -->
7
+ <header class="flex justify-between items-center mb-10 bg-gray-800 bg-opacity-50 p-4 rounded-xl border border-gray-700 backdrop-blur-sm">
8
+ <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-600">
9
+ 講師儀表板 Instructor Dashboard
10
+ </h1>
11
+ <div id="room-info" class="hidden flex items-center space-x-4">
12
+ <span class="text-gray-400">教室代碼</span>
13
+ <span id="display-room-code" class="text-3xl font-mono font-bold text-cyan-400 tracking-widest bg-gray-900 px-4 py-2 rounded-lg border border-cyan-500/30 shadow-[0_0_15px_rgba(34,211,238,0.3)]"></span>
14
+ </div>
15
+ <div id="create-room-container">
16
+ <button id="create-room-btn" class="bg-purple-600 hover:bg-purple-500 text-white font-bold py-2 px-6 rounded-lg transition-all shadow-lg shadow-purple-500/30">
17
+ 建立新教室
18
+ </button>
19
+ </div>
20
+ </header>
21
+
22
+ <!-- Student List -->
23
+ <div id="dashboard-content" class="hidden">
24
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="students-grid">
25
+ <!-- Student Cards will go here -->
26
+ <div class="text-center text-gray-500 col-span-full py-20">
27
+ 等待學員加入...
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ `;
33
+ }
34
+
35
+ export function setupInstructorEvents() {
36
+ const createBtn = document.getElementById('create-room-btn');
37
+ const roomInfo = document.getElementById('room-info');
38
+ const createContainer = document.getElementById('create-room-container');
39
+ const dashboardContent = document.getElementById('dashboard-content');
40
+ const displayRoomCode = document.getElementById('display-room-code');
41
+ const studentsGrid = document.getElementById('students-grid');
42
+
43
+ createBtn.addEventListener('click', async () => {
44
+ try {
45
+ createBtn.disabled = true;
46
+ createBtn.textContent = "建立中...";
47
+
48
+ const roomCode = await createRoom();
49
+
50
+ // UI Switch
51
+ createContainer.classList.add('hidden');
52
+ roomInfo.classList.remove('hidden');
53
+ dashboardContent.classList.remove('hidden');
54
+ displayRoomCode.textContent = roomCode;
55
+
56
+ // Subscribe to updates only after room is created
57
+ subscribeToRoom(roomCode, (students) => {
58
+ renderStudentCards(students, studentsGrid);
59
+ });
60
+
61
+ } catch (error) {
62
+ console.error(error);
63
+ alert("建立教室失敗");
64
+ createBtn.disabled = false;
65
+ }
66
+ });
67
+ }
68
+
69
+ function renderStudentCards(students, container) {
70
+ if (students.length === 0) {
71
+ container.innerHTML = '<div class="text-center text-gray-500 col-span-full py-20">尚無學員加入</div>';
72
+ return;
73
+ }
74
+
75
+ container.innerHTML = students.map(student => {
76
+ const progress = student.progress || {};
77
+ // Define levels we care about
78
+ const levels = [
79
+ { id: 'beginner', label: '初級', color: 'cyan' },
80
+ { id: 'intermediate', label: '中級', color: 'blue' },
81
+ { id: 'advanced', label: '高級', color: 'purple' }
82
+ ];
83
+
84
+ let badgesHtml = levels.map(level => {
85
+ const isCompleted = progress[level.id]?.status === 'completed';
86
+ const statusClass = isCompleted
87
+ ? `bg-${level.color}-500/20 text-${level.color}-300 border-${level.color}-500`
88
+ : 'bg-gray-700/50 text-gray-500 border-gray-700';
89
+
90
+ return `
91
+ <div class="flex items-center justify-between p-2 rounded border ${statusClass} transition-all">
92
+ <span class="text-xs font-bold">${level.label}</span>
93
+ <span class="text-xs">${isCompleted ? '✓' : '...'}</span>
94
+ </div>
95
+ `;
96
+ }).join('');
97
+
98
+ return `
99
+ <div class="bg-gray-800 bg-opacity-40 backdrop-blur rounded-xl border border-gray-700 p-4 hover:border-gray-500 transition-all">
100
+ <div class="flex items-center justify-between mb-4">
101
+ <div class="flex items-center space-x-3">
102
+ <div class="w-10 h-10 rounded-full bg-gradient-to-br from-gray-700 to-gray-600 flex items-center justify-center text-lg font-bold text-white">
103
+ ${student.nickname[0]}
104
+ </div>
105
+ <div>
106
+ <h3 class="font-bold text-white">${student.nickname}</h3>
107
+ <p class="text-xs text-gray-400">Online</p>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ <div class="space-y-2">
112
+ ${badgesHtml}
113
+ </div>
114
+ </div>
115
+ `;
116
+ }).join('');
117
+ }
src/views/LandingView.js ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRoom, joinRoom } from "../services/classroom.js";
2
+
3
+ export function renderLandingView() {
4
+ return `
5
+ <div class="min-h-screen flex flex-col items-center justify-center p-4">
6
+ <div class="max-w-md w-full bg-gray-600 bg-opacity-20 backdrop-blur-lg rounded-xl shadow-2xl p-8 border border-gray-700">
7
+ <h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-purple-500 mb-8 text-center tracking-tighter">
8
+ VIBECODING
9
+ </h1>
10
+
11
+ <!-- Student Join Form -->
12
+ <div id="student-form" class="space-y-6">
13
+ <div>
14
+ <label class="block text-gray-400 text-sm font-bold mb-2">教室代碼 (Room Code)</label>
15
+ <input type="text" id="room-code-input" class="w-full bg-gray-800 text-white border border-gray-600 rounded-lg py-3 px-4 focus:outline-none focus:border-cyan-500 transition-colors" placeholder="1234">
16
+ </div>
17
+ <div>
18
+ <label class="block text-gray-400 text-sm font-bold mb-2">您的暱稱 (Nickname)</label>
19
+ <input type="text" id="nickname-input" class="w-full bg-gray-800 text-white border border-gray-600 rounded-lg py-3 px-4 focus:outline-none focus:border-purple-500 transition-colors" placeholder="小明">
20
+ </div>
21
+ <button id="join-btn" class="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold py-3 px-4 rounded-lg transform transition hover:scale-105 active:scale-95 shadow-lg shadow-cyan-500/30">
22
+ 進入教室
23
+ </button>
24
+ </div>
25
+
26
+ <!-- Instructor Toggle -->
27
+ <div class="mt-8 pt-6 border-t border-gray-700 text-center">
28
+ <button id="instructor-mode-btn" class="text-gray-500 text-sm hover:text-cyan-400 transition-colors">
29
+ 我是講師 (Instructor Mode)
30
+ </button>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ `;
35
+ }
36
+
37
+ export function setupLandingEvents(navigateTo) {
38
+ const joinBtn = document.getElementById('join-btn');
39
+ const instructorBtn = document.getElementById('instructor-mode-btn');
40
+
41
+ joinBtn.addEventListener('click', async () => {
42
+ const roomCode = document.getElementById('room-code-input').value.trim();
43
+ const nickname = document.getElementById('nickname-input').value.trim();
44
+
45
+ if (!roomCode || !nickname) {
46
+ alert('請輸入教室代碼和暱稱');
47
+ return;
48
+ }
49
+
50
+ try {
51
+ joinBtn.textContent = '加入中...';
52
+ joinBtn.disabled = true;
53
+ const studentId = await joinRoom(roomCode, nickname);
54
+ localStorage.setItem('vibecoding_student_id', studentId);
55
+ localStorage.setItem('vibecoding_room_code', roomCode);
56
+ localStorage.setItem('vibecoding_nickname', nickname);
57
+ navigateTo('student');
58
+ } catch (error) {
59
+ alert('加入失敗: ' + error.message);
60
+ joinBtn.textContent = '進入教室';
61
+ joinBtn.disabled = false;
62
+ }
63
+ });
64
+
65
+ instructorBtn.addEventListener('click', () => {
66
+ navigateTo('instructor');
67
+ });
68
+ }
src/views/StudentView.js ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { submitPrompt } from "../services/classroom.js";
2
+
3
+ const LEVELS = [
4
+ {
5
+ id: 'beginner',
6
+ name: '初級 (Beginner)',
7
+ desc: '基礎幾何圖形繪製',
8
+ link: 'https://geminicanvas.web.app/' // TODO: Replace with real link
9
+ },
10
+ {
11
+ id: 'intermediate',
12
+ name: '中級 (Intermediate)',
13
+ desc: '迴圈與重複結構',
14
+ link: 'https://geminicanvas.web.app/'
15
+ },
16
+ {
17
+ id: 'advanced',
18
+ name: '高級 (Advanced)',
19
+ desc: '函數與參數應用',
20
+ link: 'https://geminicanvas.web.app/'
21
+ }
22
+ ];
23
+
24
+ export function renderStudentView() {
25
+ const nickname = localStorage.getItem('vibecoding_nickname') || 'Guest';
26
+
27
+ return `
28
+ <div class="min-h-screen p-6 pb-24">
29
+ <header class="flex justify-between items-center mb-8">
30
+ <div class="flex items-center space-x-2">
31
+ <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
32
+ <span class="text-gray-400 text-sm">已連線: ${nickname}</span>
33
+ </div>
34
+ <div class="text-right">
35
+ <h1 class="text-xl font-bold italic text-white tracking-widest">VIBECODING</h1>
36
+ </div>
37
+ </header>
38
+
39
+ <div class="grid grid-cols-1 gap-6 max-w-2xl mx-auto">
40
+ ${LEVELS.map(level => `
41
+ <div class="group relative bg-gray-800 bg-opacity-50 border border-gray-700 rounded-2xl overflow-hidden hover:border-cyan-500/50 transition-all duration-300">
42
+ <div class="absolute top-0 left-0 w-1 h-full bg-gray-600 group-hover:bg-cyan-400 transition-colors"></div>
43
+
44
+ <div class="p-6 pl-8">
45
+ <div class="flex justify-between items-start mb-4">
46
+ <div>
47
+ <h2 class="text-2xl font-bold text-white mb-1">${level.name}</h2>
48
+ <p class="text-gray-400 text-sm">${level.desc}</p>
49
+ </div>
50
+ <a href="${level.link}" target="_blank"
51
+ class="bg-gray-700 hover:bg-cyan-600 text-white p-2 rounded-lg transition-colors"
52
+ title="前往題目">
53
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
55
+ </svg>
56
+ </a>
57
+ </div>
58
+
59
+ <div class="mt-6">
60
+ <label class="block text-xs uppercase tracking-wider text-gray-500 mb-2">提交修復提示詞</label>
61
+ <div class="flex space-x-2">
62
+ <input type="text" id="input-${level.id}"
63
+ class="flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 focus:outline-none transition-colors"
64
+ placeholder="輸入你的 Prompt...">
65
+ <button onclick="window.submitLevel('${level.id}')"
66
+ class="bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white px-6 py-2 rounded-lg font-bold transition-transform active:scale-95 shadow-lg shadow-cyan-900/50">
67
+ 提交
68
+ </button>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ `).join('')}
74
+ </div>
75
+
76
+ <!-- Peer Learning (Floating Action Button) -->
77
+ <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"
78
+ title="查看同學作業">
79
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
80
+ <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" />
81
+ </svg>
82
+ </button>
83
+ </div>
84
+ `;
85
+ }
86
+
87
+ export function setupStudentEvents() {
88
+ window.submitLevel = async (levelId) => {
89
+ const input = document.getElementById(`input-${levelId}`);
90
+ const prompt = input.value;
91
+ const roomCode = localStorage.getItem('vibecoding_room_code');
92
+ const studentId = localStorage.getItem('vibecoding_student_id');
93
+
94
+ if (!participantDataCheck(roomCode, studentId)) return;
95
+
96
+ try {
97
+ await submitPrompt(roomCode, studentId, levelId, prompt);
98
+
99
+ // Visual feedback
100
+ const btn = input.nextElementSibling;
101
+ const originalText = btn.textContent;
102
+ btn.textContent = "✓ 已提交";
103
+ btn.classList.add("bg-green-600", "from-green-600", "to-green-600");
104
+ setTimeout(() => {
105
+ btn.textContent = originalText;
106
+ btn.classList.remove("bg-green-600", "from-green-600", "to-green-600");
107
+ }, 2000);
108
+
109
+ } catch (error) {
110
+ console.error(error);
111
+ alert("提交失敗");
112
+ }
113
+ };
114
+ }
115
+
116
+ function participantDataCheck(roomCode, studentId) {
117
+ if (!roomCode || !studentId) {
118
+ alert("連線資訊遺失,請重新登入");
119
+ window.location.reload();
120
+ return false;
121
+ }
122
+ return true;
123
+ }
124
+
125
+ // Peer Learning Modal Logic
126
+ import { getPeerPrompts } from "../services/classroom.js";
127
+
128
+ function renderPeerModal() {
129
+ return `
130
+ <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">
131
+ <div class="bg-gray-800 rounded-2xl max-w-2xl w-full max-h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
132
+ <div class="p-6 border-b border-gray-700 flex justify-between items-center">
133
+ <h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
134
+ <button onclick="closePeerModal()" class="text-gray-400 hover:text-white">
135
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
136
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
137
+ </svg>
138
+ </button>
139
+ </div>
140
+
141
+ <div class="p-6 flex-1 overflow-y-auto space-y-4" id="peer-prompts-container">
142
+ <!-- Prompts loaded here -->
143
+ </div>
144
+
145
+ <div class="p-4 border-t border-gray-700 bg-gray-900/50 rounded-b-2xl">
146
+ <div class="flex space-x-2 overflow-x-auto pb-2" id="level-tabs">
147
+ <!-- Tabs for switching levels -->
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ `;
153
+ }
154
+
155
+ window.openPeerModal = async () => {
156
+ const modal = document.getElementById('peer-modal');
157
+ modal.classList.remove('hidden');
158
+
159
+ // Default to first level or currently working level? Let's just create tabs.
160
+ renderLevelTabs();
161
+ // Load first level by default
162
+ loadPeerPrompts('beginner');
163
+ };
164
+
165
+ window.closePeerModal = () => {
166
+ document.getElementById('peer-modal').classList.add('hidden');
167
+ };
168
+
169
+ function renderLevelTabs() {
170
+ const container = document.getElementById('level-tabs');
171
+ container.innerHTML = LEVELS.map(level => `
172
+ <button onclick="loadPeerPrompts('${level.id}')"
173
+ class="px-4 py-2 rounded-full bg-gray-700 text-sm hover:bg-cyan-600 hover:text-white transition-colors whitespace-nowrap">
174
+ ${level.name.split(' ')[0]}
175
+ </button>
176
+ `).join('');
177
+ }
178
+
179
+ window.loadPeerPrompts = async (levelId) => {
180
+ const container = document.getElementById('peer-prompts-container');
181
+ container.innerHTML = '<div class="text-center text-gray-400 py-10">載入中...</div>';
182
+
183
+ const roomCode = localStorage.getItem('vibecoding_room_code');
184
+ const prompts = await getPeerPrompts(roomCode, levelId);
185
+
186
+ if (prompts.length === 0) {
187
+ container.innerHTML = '<div class="text-center text-gray-500 py-10">尚無同學提交此關卡</div>';
188
+ return;
189
+ }
190
+
191
+ container.innerHTML = prompts.map(p => `
192
+ <div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600">
193
+ <div class="flex items-center space-x-2 mb-2">
194
+ <div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
195
+ ${p.nickname[0]}
196
+ </div>
197
+ <span class="font-bold text-cyan-300 text-sm">${p.nickname}</span>
198
+ </div>
199
+ <p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50">
200
+ ${p.prompt}
201
+ </p>
202
+ </div>
203
+ `).join('');
204
+ };
205
+
206
+ // Append modal to body only if not exists
207
+ if (!document.getElementById('peer-modal')) {
208
+ const div = document.createElement('div');
209
+ div.innerHTML = renderPeerModal();
210
+ document.body.appendChild(div.firstElementChild);
211
+ }