Vibecodingex / src /views /StudentView.js
Lashtw's picture
Upload 8 files
eb6f7c1 verified
raw
history blame
12 kB
import { submitPrompt, getChallenges } from "../services/classroom.js";
import { getPeerPrompts } from "../services/classroom.js"; // Import needed for modal
// Cache challenges locally to avoid refetching heavily
let cachedChallenges = [];
export async function renderStudentView() {
const nickname = localStorage.getItem('vibecoding_nickname') || 'Guest';
// Fetch challenges if empty
if (cachedChallenges.length === 0) {
try {
cachedChallenges = await getChallenges();
} catch (e) {
console.error("Failed to fetch challenges", e);
throw new Error("無法讀取題目列表,請檢查網路連線 (Error: " + e.message + ")");
}
}
// Group by Level (optional, but good for UI organization if we wanted headers)
// For now, let's just map them.
// Or if we want to keep the headers:
const levelGroups = {
beginner: cachedChallenges.filter(c => c.level === 'beginner'),
intermediate: cachedChallenges.filter(c => c.level === 'intermediate'),
advanced: cachedChallenges.filter(c => c.level === 'advanced')
};
const levelNames = {
beginner: "初級 (Beginner)",
intermediate: "中級 (Intermediate)",
advanced: "高級 (Advanced)"
};
const renderCard = (c) => `
<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">
<div class="absolute top-0 left-0 w-1 h-full bg-gray-600 group-hover:bg-cyan-400 transition-colors"></div>
<div class="p-6 pl-8 flex-1 flex flex-col">
<div class="flex flex-col sm:flex-row justify-between items-start mb-4 gap-4">
<div>
<h2 class="text-xl font-bold text-white mb-1">${c.title}</h2>
<p class="text-gray-400 text-sm whitespace-pre-line">${c.description}</p>
</div>
<a href="${c.link}" target="_blank"
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"
title="前往題目">
<span>Go to Task</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</a>
</div>
<div class="mt-auto pt-4 border-t border-gray-700/50">
<label class="block text-xs uppercase tracking-wider text-gray-500 mb-2">提交修復提示詞</label>
<div class="flex flex-col space-y-2">
<textarea id="input-${c.id}" rows="2"
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"
placeholder="輸入你的 Prompt..."></textarea>
<div id="error-${c.id}" class="text-red-500 text-xs hidden">提示詞太短囉,請多寫一點細節!</div>
<button onclick="window.submitLevel('${c.id}')"
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">
提交
</button>
</div>
</div>
</div>
</div>
`;
// Render logic
let contentHtml = '';
if (cachedChallenges.length === 0) {
contentHtml = '<div class="text-center text-gray-500 py-10">目前沒有題目,請請講師至後台新增。</div>';
} else {
['beginner', 'intermediate', 'advanced'].forEach(levelId => {
const tasks = levelGroups[levelId] || [];
if (tasks.length > 0) {
contentHtml += `
<section>
<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">
${levelNames[levelId]}
</h3>
<div class="grid grid-cols-1 gap-6">
${tasks.map(renderCard).join('')}
</div>
</section>
`;
}
});
}
return `
<div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl">
<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">
<div class="flex flex-col">
<div class="flex items-center space-x-2">
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
<span class="text-gray-400 text-sm truncate max-w-[150px]">${nickname}</span>
</div>
<div class="text-xs text-gray-500 mt-1">教室: <span class="font-mono text-cyan-400 font-bold">${roomCode}</span></div>
</div>
<div>
<h1 class="text-xl font-bold italic text-white tracking-widest">VIBECODING</h1>
</div>
</header>
<div class="space-y-12">
${contentHtml}
</div>
<!-- Peer Learning FAB -->
<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"
title="查看同學作業">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</button>
</div>
`;
}
export function setupStudentEvents() {
window.submitLevel = async (challengeId) => {
const input = document.getElementById(`input-${challengeId}`);
const errorMsg = document.getElementById(`error-${challengeId}`);
const prompt = input.value;
const roomCode = localStorage.getItem('vibecoding_room_code');
const userId = localStorage.getItem('vibecoding_user_id');
if (!participantDataCheck(roomCode, userId)) return;
// Validation Rule: Length >= 5
if (prompt.trim().length < 5) {
errorMsg.classList.remove('hidden');
input.classList.add('border-red-500');
return;
}
// Reset error state
errorMsg.classList.add('hidden');
input.classList.remove('border-red-500');
try {
await submitPrompt(userId, roomCode, challengeId, prompt);
// Visual feedback
const container = input.parentElement;
const btn = container.querySelector('button');
const originalText = btn.textContent;
btn.textContent = "✓ 已提交";
btn.classList.add("bg-green-600", "from-green-600", "to-green-600");
setTimeout(() => {
btn.textContent = originalText;
btn.classList.remove("bg-green-600", "from-green-600", "to-green-600");
}, 2000);
} catch (error) {
console.error(error);
alert("提交失敗: " + error.message);
}
};
}
function participantDataCheck(roomCode, userId) {
if (!roomCode || !userId) {
alert("連線資訊遺失,請重新登入");
window.location.reload();
return false;
}
return true;
}
// Peer Learning Modal Logic
function renderPeerModal() {
// Dropdown options based on cachedChallenges
let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
if (cachedChallenges.length > 0) {
optionsHtml += cachedChallenges.map(c =>
`<option value="${c.id}">[${c.level}] ${c.title}</option>`
).join('');
}
return `
<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">
<div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
<button onclick="closePeerModal()" class="text-gray-400 hover:text-white">✕</button>
</div>
<div class="p-4 bg-gray-900 border-b border-gray-700">
<select id="peer-challenge-select" onchange="loadPeerPrompts(this.value)" class="w-full bg-gray-800 border border-gray-600 rounded p-2 text-white">
${optionsHtml}
</select>
</div>
<div class="p-4 flex-1 overflow-y-auto space-y-4" id="peer-prompts-container">
<div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
</div>
</div>
</div>
`;
}
window.openPeerModal = () => {
// Remove existing modal if any to ensure fresh render (especially for dropdown)
const existing = document.getElementById('peer-modal');
if (existing) existing.remove();
const div = document.createElement('div');
div.innerHTML = renderPeerModal();
document.body.appendChild(div.firstElementChild);
document.getElementById('peer-modal').classList.remove('hidden');
};
window.closePeerModal = () => {
document.getElementById('peer-modal').classList.add('hidden');
};
window.loadPeerPrompts = async (challengeId) => {
const container = document.getElementById('peer-prompts-container');
container.innerHTML = '<div class="text-center text-gray-400 py-10">載入中...</div>';
const roomCode = localStorage.getItem('vibecoding_room_code');
const prompts = await getPeerPrompts(roomCode, challengeId);
if (prompts.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-10">尚無同學提交此關卡</div>';
return;
}
container.innerHTML = prompts.map(p => `
<div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600">
<div class="flex items-center space-x-2 mb-2">
<div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
${p.nickname[0]}
</div>
<span class="font-bold text-cyan-300 text-sm">${p.nickname}</span>
<span class="text-gray-500 text-xs ml-auto">${new Date(p.timestamp.seconds * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
</div>
<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>
</div>
`).join('');
};