Spaces:
Running
Running
Upload 8 files
Browse files- src/views/InstructorView.js +320 -3
src/views/InstructorView.js
CHANGED
|
@@ -100,6 +100,68 @@ export async function renderInstructorView() {
|
|
| 100 |
</div>
|
| 101 |
</div>
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
<div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
|
| 104 |
<!-- Header -->
|
| 105 |
<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">
|
|
@@ -748,6 +810,259 @@ export function setupInstructorEvents() {
|
|
| 748 |
}
|
| 749 |
}
|
| 750 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
}
|
| 752 |
|
| 753 |
/**
|
|
@@ -785,9 +1100,9 @@ function renderTransposedHeatmap(students) {
|
|
| 785 |
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
|
| 786 |
</div>
|
| 787 |
<div class="flex items-center justify-center space-x-1">
|
| 788 |
-
<
|
| 789 |
${student.nickname}
|
| 790 |
-
</
|
| 791 |
<button onclick="window.confirmKick('${student.id}', '${student.nickname}')" class="text-gray-600 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity" title="踢出學員">
|
| 792 |
🗑️
|
| 793 |
</button>
|
|
@@ -853,7 +1168,9 @@ function renderTransposedHeatmap(students) {
|
|
| 853 |
<div class="flex items-center justify-between">
|
| 854 |
<div class="flex flex-col">
|
| 855 |
<span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-0.5">${c.level}</span>
|
| 856 |
-
<
|
|
|
|
|
|
|
| 857 |
</div>
|
| 858 |
<!-- Stats (Optional) -->
|
| 859 |
<!-- <span class="text-xs text-gray-500">0%</span> -->
|
|
|
|
| 100 |
</div>
|
| 101 |
</div>
|
| 102 |
|
| 103 |
+
<!-- Multi-Prompt Viewer Modal -->
|
| 104 |
+
<div id="prompt-list-modal" class="fixed inset-0 bg-black/95 backdrop-blur z-[60] hidden flex flex-col p-6 transition-opacity duration-300">
|
| 105 |
+
<div class="flex justify-between items-center mb-6 border-b border-gray-700 pb-4">
|
| 106 |
+
<div>
|
| 107 |
+
<h2 class="text-2xl font-bold text-cyan-400" id="prompt-list-title">提示詞列表</h2>
|
| 108 |
+
<p class="text-gray-400 text-sm" id="prompt-list-subtitle">點選下方複選框進行比較 (最多3項)</p>
|
| 109 |
+
</div>
|
| 110 |
+
<button onclick="document.getElementById('prompt-list-modal').classList.add('hidden')" class="text-gray-400 hover:text-white text-3xl">✕</button>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div id="prompt-list-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 overflow-y-auto flex-1 custom-scrollbar pb-20">
|
| 114 |
+
<!-- Dynamic Cards -->
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<!-- Floating Action Footer -->
|
| 118 |
+
<div class="absolute bottom-6 left-1/2 transform -translate-x-1/2 flex space-x-4">
|
| 119 |
+
<button id="btn-compare-prompts" class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white font-bold py-3 px-8 rounded-full shadow-2xl flex items-center space-x-2 transition-all transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
| 120 |
+
<span>🔍 比較已選項目 (0/3)</span>
|
| 121 |
+
</button>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<!-- Comparison Modal with Annotation Canvas -->
|
| 126 |
+
<div id="comparison-modal" class="fixed inset-0 bg-gray-900 z-[70] hidden flex flex-col">
|
| 127 |
+
<!-- Toolbar -->
|
| 128 |
+
<div class="bg-gray-800 p-4 border-b border-gray-700 flex justify-between items-center shadow-lg z-50">
|
| 129 |
+
<h2 class="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">
|
| 130 |
+
提示詞比較與註記
|
| 131 |
+
</h2>
|
| 132 |
+
|
| 133 |
+
<div class="flex items-center space-x-4 bg-gray-900 rounded-full px-4 py-1.5 border border-gray-600">
|
| 134 |
+
<!-- Pen Tools -->
|
| 135 |
+
<button class="annotation-tool p-2 rounded-full hover:bg-gray-700 transition-colors ring-2 ring-transparent focus:outline-none" data-color="#ef4444" onclick="setPenColor('#ef4444', this)">
|
| 136 |
+
<div class="w-4 h-4 rounded-full bg-red-500"></div>
|
| 137 |
+
</button>
|
| 138 |
+
<button class="annotation-tool p-2 rounded-full hover:bg-gray-700 transition-colors ring-2 ring-transparent focus:outline-none" data-color="#3b82f6" onclick="setPenColor('#3b82f6', this)">
|
| 139 |
+
<div class="w-4 h-4 rounded-full bg-blue-500"></div>
|
| 140 |
+
</button>
|
| 141 |
+
<button class="annotation-tool p-2 rounded-full hover:bg-gray-700 transition-colors ring-2 ring-transparent focus:outline-none" data-color="#22c55e" onclick="setPenColor('#22c55e', this)">
|
| 142 |
+
<div class="w-4 h-4 rounded-full bg-green-500"></div>
|
| 143 |
+
</button>
|
| 144 |
+
<div class="w-px h-6 bg-gray-600 mx-2"></div>
|
| 145 |
+
<button onclick="clearCanvas()" class="text-gray-400 hover:text-white text-sm font-bold px-2">
|
| 146 |
+
清除 (Clear)
|
| 147 |
+
</button>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<button onclick="closeComparison()" class="text-gray-400 hover:text-white text-3xl">✕</button>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<!-- Canvas Container -->
|
| 154 |
+
<div class="flex-1 relative overflow-hidden bg-gray-900" id="comparison-container">
|
| 155 |
+
<!-- Grid Content (Will be behind canvas) -->
|
| 156 |
+
<div id="comparison-grid" class="absolute inset-0 grid gap-0 p-0 z-0">
|
| 157 |
+
<!-- Dynamic Columns -->
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<!-- Canvas Layer -->
|
| 161 |
+
<canvas id="annotation-canvas" class="absolute inset-0 z-10 touch-none cursor-crosshair"></canvas>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
|
| 165 |
<div class="min-h-screen p-6 pb-20 bg-gray-900 text-white">
|
| 166 |
<!-- Header -->
|
| 167 |
<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">
|
|
|
|
| 810 |
}
|
| 811 |
}
|
| 812 |
});
|
| 813 |
+
// Prompt Viewer Logic
|
| 814 |
+
window.openPromptList = (type, id, title) => {
|
| 815 |
+
const modal = document.getElementById('prompt-list-modal');
|
| 816 |
+
const container = document.getElementById('prompt-list-container');
|
| 817 |
+
const titleEl = document.getElementById('prompt-list-title');
|
| 818 |
+
|
| 819 |
+
titleEl.textContent = type === 'student' ? `${title} 的所有提示詞` : `題目:${title} 的所有作品`;
|
| 820 |
+
container.innerHTML = '';
|
| 821 |
+
modal.classList.remove('hidden');
|
| 822 |
+
|
| 823 |
+
// Collect Prompts
|
| 824 |
+
let prompts = [];
|
| 825 |
+
|
| 826 |
+
if (type === 'student') {
|
| 827 |
+
const student = currentStudents.find(s => s.id === id);
|
| 828 |
+
if (student && student.progress) {
|
| 829 |
+
prompts = Object.entries(student.progress)
|
| 830 |
+
.filter(([_, p]) => p.status === 'completed' && p.prompt)
|
| 831 |
+
.map(([challengeId, p]) => {
|
| 832 |
+
const challenge = cachedChallenges.find(c => c.id === challengeId);
|
| 833 |
+
return {
|
| 834 |
+
id: `${student.id}_${challengeId}`,
|
| 835 |
+
title: challenge ? challenge.title : '未知題目',
|
| 836 |
+
prompt: p.prompt,
|
| 837 |
+
author: student.nickname,
|
| 838 |
+
time: p.completedAt ? new Date(p.completedAt.seconds * 1000).toLocaleString() : ''
|
| 839 |
+
};
|
| 840 |
+
});
|
| 841 |
+
}
|
| 842 |
+
} else if (type === 'challenge') {
|
| 843 |
+
currentStudents.forEach(student => {
|
| 844 |
+
if (student.progress && student.progress[id]) {
|
| 845 |
+
const p = student.progress[id];
|
| 846 |
+
if (p.status === 'completed' && p.prompt) {
|
| 847 |
+
prompts.push({
|
| 848 |
+
id: `${student.id}_${id}`,
|
| 849 |
+
title: student.nickname, // When viewing challenge, title is student name
|
| 850 |
+
prompt: p.prompt,
|
| 851 |
+
author: student.nickname,
|
| 852 |
+
time: p.completedAt ? new Date(p.completedAt.seconds * 1000).toLocaleString() : ''
|
| 853 |
+
});
|
| 854 |
+
}
|
| 855 |
+
}
|
| 856 |
+
});
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
if (prompts.length === 0) {
|
| 860 |
+
container.innerHTML = '<div class="col-span-full text-center text-gray-500 py-10">無資料</div>';
|
| 861 |
+
return;
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
prompts.forEach(p => {
|
| 865 |
+
const card = document.createElement('div');
|
| 866 |
+
card.className = 'bg-gray-800 rounded-xl p-4 border border-gray-700 hover:border-cyan-500 transition-colors flex flex-col h-64';
|
| 867 |
+
card.innerHTML = `
|
| 868 |
+
<div class="flex justify-between items-start mb-2">
|
| 869 |
+
<h3 class="font-bold text-white truncate w-3/4" title="${p.title}">${p.title}</h3>
|
| 870 |
+
<!-- Checkbox Placeholder for Phase 2 -->
|
| 871 |
+
<input type="checkbox" class="w-5 h-5 rounded border-gray-600 text-purple-600 focus:ring-purple-500 bg-gray-700 prompt-select-checkbox cursor-pointer"
|
| 872 |
+
data-id="${p.id}"
|
| 873 |
+
onchange="handlePromptSelection(this)">
|
| 874 |
+
</div>
|
| 875 |
+
<div class="bg-black/30 rounded p-3 flex-1 overflow-y-auto text-xs font-mono text-green-300 mb-2 whitespace-pre-wrap">${p.prompt}</div>
|
| 876 |
+
<div class="text-[10px] text-gray-500 text-right">${p.time}</div>
|
| 877 |
+
`;
|
| 878 |
+
container.appendChild(card);
|
| 879 |
+
});
|
| 880 |
+
};
|
| 881 |
+
|
| 882 |
+
// Selection Logic
|
| 883 |
+
let selectedPrompts = []; // Stores IDs
|
| 884 |
+
|
| 885 |
+
window.handlePromptSelection = (checkbox) => {
|
| 886 |
+
const id = checkbox.dataset.id;
|
| 887 |
+
|
| 888 |
+
if (checkbox.checked) {
|
| 889 |
+
if (selectedPrompts.length >= 3) {
|
| 890 |
+
checkbox.checked = false;
|
| 891 |
+
alert('最多只能選擇 3 個提示詞進行比較');
|
| 892 |
+
return;
|
| 893 |
+
}
|
| 894 |
+
selectedPrompts.push(id);
|
| 895 |
+
} else {
|
| 896 |
+
selectedPrompts = selectedPrompts.filter(pid => pid !== id);
|
| 897 |
+
}
|
| 898 |
+
updateCompareButton();
|
| 899 |
+
};
|
| 900 |
+
|
| 901 |
+
function updateCompareButton() {
|
| 902 |
+
const btn = document.getElementById('btn-compare-prompts');
|
| 903 |
+
if (!btn) return;
|
| 904 |
+
|
| 905 |
+
const count = selectedPrompts.length;
|
| 906 |
+
const span = btn.querySelector('span');
|
| 907 |
+
if (span) span.textContent = `🔍 比較已選項目 (${count}/3)`;
|
| 908 |
+
|
| 909 |
+
if (count > 0) {
|
| 910 |
+
btn.disabled = false;
|
| 911 |
+
btn.classList.remove('opacity-50', 'cursor-not-allowed');
|
| 912 |
+
} else {
|
| 913 |
+
btn.disabled = true;
|
| 914 |
+
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
| 915 |
+
}
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
// Comparison Logic
|
| 919 |
+
const compareBtn = document.getElementById('btn-compare-prompts');
|
| 920 |
+
if (compareBtn) {
|
| 921 |
+
compareBtn.addEventListener('click', () => {
|
| 922 |
+
const dataToCompare = [];
|
| 923 |
+
selectedPrompts.forEach(fullId => {
|
| 924 |
+
const lastUnderscore = fullId.lastIndexOf('_');
|
| 925 |
+
const studentId = fullId.substring(0, lastUnderscore);
|
| 926 |
+
const challengeId = fullId.substring(lastUnderscore + 1);
|
| 927 |
+
|
| 928 |
+
const student = currentStudents.find(s => s.id === studentId);
|
| 929 |
+
if (student && student.progress && student.progress[challengeId]) {
|
| 930 |
+
const p = student.progress[challengeId];
|
| 931 |
+
const challenge = cachedChallenges.find(c => c.id === challengeId);
|
| 932 |
+
|
| 933 |
+
dataToCompare.push({
|
| 934 |
+
title: challenge ? challenge.title : '未知',
|
| 935 |
+
author: student.nickname,
|
| 936 |
+
prompt: p.prompt,
|
| 937 |
+
time: p.completedAt ? new Date(p.completedAt.seconds * 1000).toLocaleTimeString() : ''
|
| 938 |
+
});
|
| 939 |
+
}
|
| 940 |
+
});
|
| 941 |
+
|
| 942 |
+
openComparisonView(dataToCompare);
|
| 943 |
+
});
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
window.openComparisonView = (items) => {
|
| 947 |
+
const modal = document.getElementById('comparison-modal');
|
| 948 |
+
const grid = document.getElementById('comparison-grid');
|
| 949 |
+
|
| 950 |
+
// Setup Grid Columns
|
| 951 |
+
let colClass = 'grid-cols-1';
|
| 952 |
+
if (items.length === 2) colClass = 'grid-cols-2';
|
| 953 |
+
if (items.length === 3) colClass = 'grid-cols-3';
|
| 954 |
+
|
| 955 |
+
grid.className = `absolute inset-0 grid ${colClass} gap-0 divide-x divide-gray-700`;
|
| 956 |
+
grid.innerHTML = '';
|
| 957 |
+
|
| 958 |
+
items.forEach(item => {
|
| 959 |
+
const col = document.createElement('div');
|
| 960 |
+
col.className = 'flex flex-col h-full bg-gray-900 p-6';
|
| 961 |
+
col.innerHTML = `
|
| 962 |
+
<div class="mb-4 border-b border-gray-700 pb-2">
|
| 963 |
+
<h3 class="text-lg font-bold text-cyan-400">${item.author}</h3>
|
| 964 |
+
<p class="text-sm text-gray-400 truncate">${item.title}</p>
|
| 965 |
+
</div>
|
| 966 |
+
<!-- Prompt Content: Large Text for reading -->
|
| 967 |
+
<div class="flex-1 overflow-y-auto font-mono text-green-300 text-lg leading-relaxed whitespace-pre-wrap p-2 hover:bg-white/5 transition-colors rounded">
|
| 968 |
+
${item.prompt}
|
| 969 |
+
</div>
|
| 970 |
+
`;
|
| 971 |
+
grid.appendChild(col);
|
| 972 |
+
});
|
| 973 |
+
|
| 974 |
+
document.getElementById('prompt-list-modal').classList.add('hidden');
|
| 975 |
+
modal.classList.remove('hidden');
|
| 976 |
+
|
| 977 |
+
// Init Canvas (Phase 3)
|
| 978 |
+
setTimeout(setupCanvas, 100);
|
| 979 |
+
};
|
| 980 |
+
|
| 981 |
+
window.closeComparison = () => {
|
| 982 |
+
document.getElementById('comparison-modal').classList.add('hidden');
|
| 983 |
+
clearCanvas();
|
| 984 |
+
};
|
| 985 |
+
|
| 986 |
+
// --- Phase 3: Annotation Tools ---
|
| 987 |
+
let canvas, ctx;
|
| 988 |
+
let isDrawing = false;
|
| 989 |
+
let currentPenColor = '#ef4444'; // Red default
|
| 990 |
+
|
| 991 |
+
window.setupCanvas = () => {
|
| 992 |
+
canvas = document.getElementById('annotation-canvas');
|
| 993 |
+
const container = document.getElementById('comparison-container');
|
| 994 |
+
if (!canvas || !container) return;
|
| 995 |
+
|
| 996 |
+
ctx = canvas.getContext('2d');
|
| 997 |
+
|
| 998 |
+
// Resize
|
| 999 |
+
const resize = () => {
|
| 1000 |
+
canvas.width = container.clientWidth;
|
| 1001 |
+
canvas.height = container.clientHeight;
|
| 1002 |
+
ctx.lineCap = 'round';
|
| 1003 |
+
ctx.lineJoin = 'round';
|
| 1004 |
+
ctx.strokeStyle = currentPenColor;
|
| 1005 |
+
ctx.lineWidth = 3;
|
| 1006 |
+
};
|
| 1007 |
+
resize();
|
| 1008 |
+
window.addEventListener('resize', resize);
|
| 1009 |
+
|
| 1010 |
+
// Drawing Events
|
| 1011 |
+
const start = (e) => {
|
| 1012 |
+
isDrawing = true;
|
| 1013 |
+
ctx.beginPath();
|
| 1014 |
+
const { x, y } = getPos(e);
|
| 1015 |
+
ctx.moveTo(x, y);
|
| 1016 |
+
};
|
| 1017 |
+
|
| 1018 |
+
const move = (e) => {
|
| 1019 |
+
if (!isDrawing) return;
|
| 1020 |
+
const { x, y } = getPos(e);
|
| 1021 |
+
ctx.lineTo(x, y);
|
| 1022 |
+
ctx.stroke();
|
| 1023 |
+
};
|
| 1024 |
+
|
| 1025 |
+
const end = () => {
|
| 1026 |
+
isDrawing = false;
|
| 1027 |
+
};
|
| 1028 |
+
|
| 1029 |
+
canvas.onmousedown = start;
|
| 1030 |
+
canvas.onmousemove = move;
|
| 1031 |
+
canvas.onmouseup = end;
|
| 1032 |
+
canvas.onmouseleave = end;
|
| 1033 |
+
|
| 1034 |
+
// Touch support
|
| 1035 |
+
canvas.ontouchstart = (e) => { e.preventDefault(); start(e.touches[0]); };
|
| 1036 |
+
canvas.ontouchmove = (e) => { e.preventDefault(); move(e.touches[0]); };
|
| 1037 |
+
canvas.ontouchend = (e) => { e.preventDefault(); end(); };
|
| 1038 |
+
};
|
| 1039 |
+
|
| 1040 |
+
function getPos(e) {
|
| 1041 |
+
const rect = canvas.getBoundingClientRect();
|
| 1042 |
+
return {
|
| 1043 |
+
x: e.clientX - rect.left,
|
| 1044 |
+
y: e.clientY - rect.top
|
| 1045 |
+
};
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
window.setPenColor = (color, btn) => {
|
| 1049 |
+
currentPenColor = color;
|
| 1050 |
+
if (ctx) ctx.strokeStyle = color;
|
| 1051 |
+
|
| 1052 |
+
// UI Update
|
| 1053 |
+
document.querySelectorAll('.annotation-tool').forEach(b => {
|
| 1054 |
+
b.classList.remove('ring-white');
|
| 1055 |
+
b.classList.add('ring-transparent');
|
| 1056 |
+
});
|
| 1057 |
+
btn.classList.remove('ring-transparent');
|
| 1058 |
+
btn.classList.add('ring-white');
|
| 1059 |
+
};
|
| 1060 |
+
|
| 1061 |
+
window.clearCanvas = () => {
|
| 1062 |
+
if (canvas && ctx) {
|
| 1063 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 1064 |
+
}
|
| 1065 |
+
};
|
| 1066 |
}
|
| 1067 |
|
| 1068 |
/**
|
|
|
|
| 1100 |
<div class="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 border-2 border-gray-800 rounded-full"></div>
|
| 1101 |
</div>
|
| 1102 |
<div class="flex items-center justify-center space-x-1">
|
| 1103 |
+
<button onclick="window.openPromptList('student', '${student.id}', '${student.nickname}')" class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr hover:text-cyan-400 hover:font-bold transition-all" style="writing-mode: vertical-rl; text-orientation: mixed;" title="查看該學員所有提示詞">
|
| 1104 |
${student.nickname}
|
| 1105 |
+
</button>
|
| 1106 |
<button onclick="window.confirmKick('${student.id}', '${student.nickname}')" class="text-gray-600 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity" title="踢出學員">
|
| 1107 |
🗑️
|
| 1108 |
</button>
|
|
|
|
| 1168 |
<div class="flex items-center justify-between">
|
| 1169 |
<div class="flex flex-col">
|
| 1170 |
<span class="text-xs text-${color}-400 font-bold uppercase tracking-wider mb-0.5">${c.level}</span>
|
| 1171 |
+
<button onclick="window.openPromptList('challenge', '${c.id}', '${c.title}')" class="font-bold text-white text-sm truncate max-w-[180px] text-left hover:text-cyan-400 transition-colors" title="查看此題目所有作品">
|
| 1172 |
+
${index + 1}. ${c.title}
|
| 1173 |
+
</button>
|
| 1174 |
</div>
|
| 1175 |
<!-- Stats (Optional) -->
|
| 1176 |
<!-- <span class="text-xs text-gray-500">0%</span> -->
|