Lashtw commited on
Commit
5d86f22
·
verified ·
1 Parent(s): 684f9a1

Upload 8 files

Browse files
Files changed (1) hide show
  1. src/views/StudentView.js +149 -117
src/views/StudentView.js CHANGED
@@ -1,80 +1,111 @@
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" />
@@ -85,22 +116,37 @@ export function renderStudentView() {
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");
@@ -108,13 +154,13 @@ export function setupStudentEvents() {
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;
@@ -123,65 +169,59 @@ function participantDataCheck(roomCode, studentId) {
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>';
@@ -195,17 +235,9 @@ window.loadPeerPrompts = async (levelId) => {
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
- }
 
1
+ import { submitPrompt, getChallenges } from "../services/classroom.js";
2
+ import { getPeerPrompts } from "../services/classroom.js"; // Import needed for modal
3
+
4
+ // Cache challenges locally to avoid refetching heavily
5
+ let cachedChallenges = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ export async function renderStudentView() {
8
  const nickname = localStorage.getItem('vibecoding_nickname') || 'Guest';
9
 
10
+ // Fetch challenges if empty
11
+ if (cachedChallenges.length === 0) {
12
+ cachedChallenges = await getChallenges();
13
+ }
14
+
15
+ // Group by Level (optional, but good for UI organization if we wanted headers)
16
+ // For now, let's just map them.
17
+ // Or if we want to keep the headers:
18
+ const levelGroups = {
19
+ beginner: cachedChallenges.filter(c => c.level === 'beginner'),
20
+ intermediate: cachedChallenges.filter(c => c.level === 'intermediate'),
21
+ advanced: cachedChallenges.filter(c => c.level === 'advanced')
22
+ };
23
+
24
+ const levelNames = {
25
+ beginner: "初級 (Beginner)",
26
+ intermediate: "中級 (Intermediate)",
27
+ advanced: "高級 (Advanced)"
28
+ };
29
+
30
+ const renderCard = (c) => `
31
+ <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 flex flex-col">
32
+ <div class="absolute top-0 left-0 w-1 h-full bg-gray-600 group-hover:bg-cyan-400 transition-colors"></div>
33
+
34
+ <div class="p-6 pl-8 flex-1 flex flex-col">
35
+ <div class="flex flex-col sm:flex-row justify-between items-start mb-4 gap-4">
36
+ <div>
37
+ <h2 class="text-xl font-bold text-white mb-1">${c.title}</h2>
38
+ <p class="text-gray-400 text-sm whitespace-pre-line">${c.description}</p>
39
+ </div>
40
+ <a href="${c.link}" target="_blank"
41
+ class="w-full sm:w-auto text-center bg-gray-700 hover:bg-cyan-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center space-x-2"
42
+ title="前往題目">
43
+ <span>Go to Task</span>
44
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
45
+ <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" />
46
+ </svg>
47
+ </a>
48
+ </div>
49
+
50
+ <div class="mt-auto pt-4 border-t border-gray-700/50">
51
+ <label class="block text-xs uppercase tracking-wider text-gray-500 mb-2">提交修復提示詞</label>
52
+ <div class="flex flex-col space-y-2">
53
+ <textarea id="input-${c.id}" rows="2"
54
+ class="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:border-cyan-500 focus:outline-none transition-colors text-sm"
55
+ placeholder="輸入你的 Prompt..."></textarea>
56
+
57
+ <div id="error-${c.id}" class="text-red-500 text-xs hidden">提示詞太短囉,請多寫一點細節!</div>
58
+
59
+ <button onclick="window.submitLevel('${c.id}')"
60
+ class="w-full 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">
61
+ 提交
62
+ </button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ `;
68
+
69
+ // Render logic
70
+ let contentHtml = '';
71
+ if (cachedChallenges.length === 0) {
72
+ contentHtml = '<div class="text-center text-gray-500 py-10">目前沒有題目,請請講師至後台新增。</div>';
73
+ } else {
74
+ ['beginner', 'intermediate', 'advanced'].forEach(levelId => {
75
+ const tasks = levelGroups[levelId] || [];
76
+ if (tasks.length > 0) {
77
+ contentHtml += `
78
+ <section>
79
+ <h3 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400 mb-4 px-2">
80
+ ${levelNames[levelId]}
81
+ </h3>
82
+ <div class="grid grid-cols-1 gap-6">
83
+ ${tasks.map(renderCard).join('')}
84
+ </div>
85
+ </section>
86
+ `;
87
+ }
88
+ });
89
+ }
90
+
91
  return `
92
+ <div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl">
93
+ <header class="flex justify-between items-center mb-8 sticky top-0 bg-slate-900/90 backdrop-blur z-20 py-4 px-2 -mx-2">
94
  <div class="flex items-center space-x-2">
95
  <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
96
+ <span class="text-gray-400 text-sm truncate max-w-[150px]">${nickname}</span>
97
  </div>
98
+ <div>
99
  <h1 class="text-xl font-bold italic text-white tracking-widest">VIBECODING</h1>
100
  </div>
101
  </header>
102
 
103
+ <div class="space-y-12">
104
+ ${contentHtml}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
106
 
107
+ <!-- Peer Learning FAB -->
108
+ <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 z-40"
109
  title="查看同學作業">
110
  <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
111
  <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" />
 
116
  }
117
 
118
  export function setupStudentEvents() {
119
+ window.submitLevel = async (challengeId) => {
120
+ const input = document.getElementById(`input-${challengeId}`);
121
+ const errorMsg = document.getElementById(`error-${challengeId}`);
122
  const prompt = input.value;
123
  const roomCode = localStorage.getItem('vibecoding_room_code');
124
+ const userId = localStorage.getItem('vibecoding_user_id');
125
+
126
+ if (!participantDataCheck(roomCode, userId)) return;
127
+
128
+ // Validation Rule: Length >= 5
129
+ if (prompt.trim().length < 5) {
130
+ errorMsg.classList.remove('hidden');
131
+ input.classList.add('border-red-500');
132
+ return;
133
+ }
134
 
135
+ // Reset error state
136
+ errorMsg.classList.add('hidden');
137
+ input.classList.remove('border-red-500');
138
 
139
  try {
140
+ await submitPrompt(userId, roomCode, challengeId, prompt);
141
 
142
  // Visual feedback
143
+ const container = input.parentElement;
144
+ const btn = container.querySelector('button');
145
  const originalText = btn.textContent;
146
+
147
  btn.textContent = "✓ 已提交";
148
  btn.classList.add("bg-green-600", "from-green-600", "to-green-600");
149
+
150
  setTimeout(() => {
151
  btn.textContent = originalText;
152
  btn.classList.remove("bg-green-600", "from-green-600", "to-green-600");
 
154
 
155
  } catch (error) {
156
  console.error(error);
157
+ alert("提交失敗: " + error.message);
158
  }
159
  };
160
  }
161
 
162
+ function participantDataCheck(roomCode, userId) {
163
+ if (!roomCode || !userId) {
164
  alert("連線資訊遺失,請重新登入");
165
  window.location.reload();
166
  return false;
 
169
  }
170
 
171
  // Peer Learning Modal Logic
 
 
172
  function renderPeerModal() {
173
+ // Dropdown options based on cachedChallenges
174
+ let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
175
+ if (cachedChallenges.length > 0) {
176
+ optionsHtml += cachedChallenges.map(c =>
177
+ `<option value="${c.id}">[${c.level}] ${c.title}</option>`
178
+ ).join('');
179
+ }
180
+
181
  return `
182
  <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">
183
+ <div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
184
  <div class="p-6 border-b border-gray-700 flex justify-between items-center">
185
  <h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
186
+ <button onclick="closePeerModal()" class="text-gray-400 hover:text-white">✕</button>
 
 
 
 
187
  </div>
188
 
189
+ <div class="p-4 bg-gray-900 border-b border-gray-700">
190
+ <select id="peer-challenge-select" onchange="loadPeerPrompts(this.value)" class="w-full bg-gray-800 border border-gray-600 rounded p-2 text-white">
191
+ ${optionsHtml}
192
+ </select>
193
  </div>
194
+
195
+ <div class="p-4 flex-1 overflow-y-auto space-y-4" id="peer-prompts-container">
196
+ <div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
 
 
197
  </div>
198
  </div>
199
  </div>
200
  `;
201
  }
202
 
203
+ window.openPeerModal = () => {
204
+ // Remove existing modal if any to ensure fresh render (especially for dropdown)
205
+ const existing = document.getElementById('peer-modal');
206
+ if (existing) existing.remove();
207
 
208
+ const div = document.createElement('div');
209
+ div.innerHTML = renderPeerModal();
210
+ document.body.appendChild(div.firstElementChild);
211
+
212
+ document.getElementById('peer-modal').classList.remove('hidden');
213
  };
214
 
215
  window.closePeerModal = () => {
216
  document.getElementById('peer-modal').classList.add('hidden');
217
  };
218
 
219
+ window.loadPeerPrompts = async (challengeId) => {
 
 
 
 
 
 
 
 
 
 
220
  const container = document.getElementById('peer-prompts-container');
221
  container.innerHTML = '<div class="text-center text-gray-400 py-10">載入中...</div>';
222
 
223
  const roomCode = localStorage.getItem('vibecoding_room_code');
224
+ const prompts = await getPeerPrompts(roomCode, challengeId);
225
 
226
  if (prompts.length === 0) {
227
  container.innerHTML = '<div class="text-center text-gray-500 py-10">尚無同學提交此關卡</div>';
 
235
  ${p.nickname[0]}
236
  </div>
237
  <span class="font-bold text-cyan-300 text-sm">${p.nickname}</span>
238
+ <span class="text-gray-500 text-xs ml-auto">${new Date(p.timestamp.seconds * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
239
  </div>
240
+ <p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
 
 
241
  </div>
242
  `).join('');
243
  };