Lashtw commited on
Commit
926bdee
·
verified ·
1 Parent(s): 7fa04e8

Upload 8 files

Browse files
Files changed (1) hide show
  1. 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
- <span class="text-xs text-gray-300 font-medium truncate max-w-[80px] writing-vertical-lr" style="writing-mode: vertical-rl; text-orientation: mixed;">
789
  ${student.nickname}
790
- </span>
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
- <span class="font-bold text-white text-sm truncate max-w-[180px]" title="${c.title}">${index + 1}. ${c.title}</span>
 
 
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> -->