Lashtw commited on
Commit
a7871cd
·
verified ·
1 Parent(s): 2b1277d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +315 -19
index.html CHANGED
@@ -1,19 +1,315 @@
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-Hant">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>即時互動文字雲 - 教師主控端</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: 'Noto Sans TC', 'Microsoft JhengHei', sans-serif;
11
+ background: #F1F5F9; color: #1E293B; min-height: 100vh;
12
+ }
13
+ header {
14
+ background: #fff; padding: 14px 22px;
15
+ border-bottom: 2px solid #E2E8F0;
16
+ display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
17
+ }
18
+ header h1 { font-size: 20px; font-weight: 800; color: #F97316; }
19
+ .badge { background: #F97316; color: #fff; font-size: 16px; font-weight: 700; padding: 6px 14px; border-radius: 20px; }
20
+ .btn { padding: 7px 16px; border: none; border-radius: 8px; background: #E2E8F0; color: #1E293B; cursor: pointer; font-size: 14px; transition: .2s; }
21
+ .btn:hover { filter: brightness(.94); }
22
+ .btn.primary { background: #F97316; color: #fff; }
23
+ .btn.danger { background: #EF4444; color: #fff; }
24
+ .qr-box { margin-left: auto; text-align: right; font-size: 12px; color: #475569; }
25
+
26
+ #question-bar {
27
+ display: flex; align-items: center; gap: 12px;
28
+ padding: 12px 22px; background: #FFF7ED; border-bottom: 1px solid #FED7AA;
29
+ }
30
+ #question-bar input {
31
+ flex: 1; padding: 9px 12px; font-size: 16px;
32
+ border: 1px solid #FDBA74; border-radius: 8px; outline: none;
33
+ }
34
+ #question-preview { font-size: 14px; color: #EA580C; }
35
+
36
+ #cloud-container {
37
+ position: relative; width: calc(100% - 32px);
38
+ height: calc(100vh - 300px); min-height: 420px;
39
+ margin: 16px; background: #fff; border-radius: 16px; overflow: hidden;
40
+ box-shadow: 0 4px 20px rgba(0,0,0,.06);
41
+ }
42
+ #word-canvas { width: 100%; height: 100%; display: block; cursor: pointer; }
43
+
44
+ #stats { padding: 0 22px 10px; font-size: 14px; color: #475569; }
45
+ #stats b { color: #F97316; font-size: 18px; }
46
+
47
+ /* 詞彙選單 overlay */
48
+ .overlay {
49
+ display: none; position: fixed; inset: 0; z-index: 1000;
50
+ background: rgba(15,23,42,.55); align-items: center; justify-content: center;
51
+ }
52
+ .panel {
53
+ background: #fff; padding: 28px 32px; border-radius: 20px;
54
+ max-width: 560px; width: 90%; max-height: 72vh; overflow-y: auto;
55
+ box-shadow: 0 20px 60px rgba(0,0,0,.3);
56
+ }
57
+ .panel h3 { margin-bottom: 16px; color: #1E293B; }
58
+ .word-chip {
59
+ display: inline-block; margin: 5px; padding: 8px 14px; font-size: 15px;
60
+ background: #FEF3C7; border: 1px solid #FCD34D; border-radius: 20px; cursor: pointer;
61
+ }
62
+ .word-chip:hover { background: #FDE68A; }
63
+ .panel-close { margin-top: 16px; text-align: right; }
64
+
65
+ /* 放大卡 */
66
+ #zoom-card { text-align: center; }
67
+ #zoom-text { font-size: 60px; font-weight: 900; color: #1E293B; word-break: break-word; }
68
+ #zoom-actions { margin-top: 28px; display: flex; gap: 12px; justify-content: center; }
69
+
70
+ /* 浮水印超連結列(DOM) */
71
+ #credit-bar {
72
+ position: fixed; left: 14px; bottom: 12px; z-index: 999;
73
+ background: rgba(255,255,255,.9); padding: 6px 14px; border-radius: 8px;
74
+ border: 1px solid #d5dbe5; font-size: 13px; color: #475569;
75
+ }
76
+ #credit-bar a { color: #4361a8; text-decoration: none; font-weight: 600; }
77
+ #credit-bar a:hover { text-decoration: underline; }
78
+ </style>
79
+ </head>
80
+ <body>
81
+
82
+ <header>
83
+ <h1>☁️ 即時互動文字雲</h1>
84
+ <span class="badge">房號:<span id="room-span">----</span></span>
85
+ <button class="btn" id="btn-lock">🔒 鎖定房間</button>
86
+ <button class="btn" id="btn-multiple">多重送出:關</button>
87
+ <button class="btn" id="btn-fullscreen">⛶ 全螢幕</button>
88
+ <button class="btn" id="btn-screenshot">📸 截圖</button>
89
+ <button class="btn" id="btn-csv">📄 匯出 CSV</button>
90
+ <button class="btn danger" id="btn-reset">🗑 清除重來</button>
91
+ <div class="qr-box"><div id="qr"></div><div id="join-link" style="max-width:180px;word-break:break-all;margin-top:4px;"></div></div>
92
+ </header>
93
+
94
+ <div id="question-bar">
95
+ <input id="q-input" type="text" placeholder="輸入討論題目,學生端將即時同步顯示…">
96
+ <button class="btn primary" id="q-send">送出題目</button>
97
+ <span id="question-preview"></span>
98
+ </div>
99
+
100
+ <div id="stats">總詞彙數量:<b id="word-count">0</b></div>
101
+
102
+ <div id="cloud-container"><canvas id="word-canvas"></canvas></div>
103
+
104
+ <!-- 詞彙選單 -->
105
+ <div class="overlay" id="pick-overlay">
106
+ <div class="panel">
107
+ <h3>選擇要放大��詞彙:</h3>
108
+ <div id="pick-list"></div>
109
+ <div class="panel-close"><button class="btn" onclick="hidePick()">取消</button></div>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- 放大卡 -->
114
+ <div class="overlay" id="zoom-overlay">
115
+ <div class="panel" id="zoom-card">
116
+ <div id="zoom-text"></div>
117
+ <div id="zoom-actions">
118
+ <button class="btn danger" id="zoom-delete">🗑 刪除此詞</button>
119
+ <button class="btn" id="zoom-close">關閉</button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- 浮水印:只有「臉書社團:萬物皆數」是連結 -->
125
+ <div id="credit-bar">
126
+ 程式設計者:精華國中 藍星宇 ·
127
+ <a href="https://www.facebook.com/groups/1554372228718393" target="_blank" rel="noopener">臉書社團:萬物皆數</a>
128
+ </div>
129
+
130
+ <script type="module">
131
+ import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js';
132
+ import { getFirestore, doc, setDoc, updateDoc, deleteField, onSnapshot } from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js';
133
+
134
+ const firebaseConfig = {
135
+ apiKey: "AIzaSyBaq7kJWERsUtcP6U8ugJK6YbdpXkqwRw0",
136
+ authDomain: "awei-lab.firebaseapp.com",
137
+ projectId: "awei-lab",
138
+ storageBucket: "awei-lab.firebasestorage.app",
139
+ messagingSenderId: "63459297039",
140
+ appId: "1:63459297039:web:d904ef831a4df32f4ae07a"
141
+ };
142
+
143
+ const app = initializeApp(firebaseConfig);
144
+ const db = getFirestore(app);
145
+
146
+ const roomCode = Math.floor(1000 + Math.random() * 9000).toString();
147
+ let allowMultiple = false;
148
+ let isLocked = false;
149
+ let words = {};
150
+ let isDrawing = false;
151
+ let drawQueue = false;
152
+ let selectedWord = null;
153
+
154
+ const canvas = document.getElementById('word-canvas');
155
+ const container = document.getElementById('cloud-container');
156
+ const roomRef = doc(db, 'rooms', roomCode);
157
+
158
+ await setDoc(roomRef, { roomCode, locked: false, allowMultiple: false, words: {}, question: '' }, { merge: true });
159
+ document.getElementById('room-span').textContent = roomCode;
160
+
161
+ onSnapshot(roomRef, (snap) => {
162
+ const data = snap.data();
163
+ if (!data) return;
164
+ words = data.words || {};
165
+ isLocked = data.locked || false;
166
+ allowMultiple = data.allowMultiple || false;
167
+ document.getElementById('question-preview').textContent = data.question ? '📌 目前題目:' + data.question : '';
168
+ document.getElementById('word-count').textContent = Object.keys(words).length;
169
+ document.getElementById('btn-lock').textContent = isLocked ? '🔓 解除鎖定' : '🔒 鎖定房間';
170
+ document.getElementById('btn-multiple').textContent = '多重送出:' + (allowMultiple ? '開' : '關');
171
+ queueDraw();
172
+ });
173
+
174
+ function queueDraw() {
175
+ if (isDrawing) { drawQueue = true; return; }
176
+ drawCloud();
177
+ }
178
+
179
+ function drawCloud() {
180
+ canvas.width = container.clientWidth;
181
+ canvas.height = container.clientHeight;
182
+ const list = Object.entries(words).map(([word, count]) => [word, count]);
183
+
184
+ if (list.length === 0) {
185
+ const ctx = canvas.getContext('2d');
186
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
187
+ ctx.font = '24px "Noto Sans TC"';
188
+ ctx.fillStyle = '#CBD5E1'; ctx.textAlign = 'center';
189
+ ctx.fillText('等待學生送出詞彙…', canvas.width/2, canvas.height/2);
190
+ return;
191
+ }
192
+
193
+ isDrawing = true;
194
+ import('https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.0.2/wordcloud2.min.js')
195
+ .then(() => WordCloud(canvas, {
196
+ list, gridSize: 8,
197
+ weightFactor: (w) => Math.max(1, Math.min(40, Math.pow(w, 1.3) * 12)),
198
+ fontFamily: 'Noto Sans TC, Microsoft JhengHei, sans-serif',
199
+ color: () => ['#F97316','#3B82F6','#10B981','#8B5CF6','#EF4444','#F59E0B','#06B6D4'][Math.floor(Math.random()*7)],
200
+ rotateRatio: 0.2, minSize: 0, shape: 'circle', ellipticity: 0.7,
201
+ backgroundColor: '#ffffff'
202
+ }));
203
+ }
204
+
205
+ canvas.addEventListener('wordcloudstop', function onStop() {
206
+ drawWatermark();
207
+ isDrawing = false;
208
+ canvas.removeEventListener('wordcloudstop', onStop);
209
+ if (drawQueue) { drawQueue = false; setTimeout(drawCloud, 500); }
210
+ });
211
+
212
+ // 浮水印畫在 canvas(僅示意;真正可點連結在下方 DOM)
213
+ function drawWatermark() {
214
+ const ctx = canvas.getContext('2d');
215
+ const w = canvas.width, h = canvas.height;
216
+ const fontSize = Math.max(10, Math.min(15, w/70));
217
+ ctx.textAlign = 'right'; ctx.textBaseline = 'bottom';
218
+ ctx.font = `bold ${fontSize}px 'Noto Sans TC'`;
219
+ ctx.fillStyle = 'rgba(100,100,100,0.45)';
220
+ ctx.fillText('程式設計者:精華國中 藍星宇', w - 16, h - 14);
221
+ ctx.font = `500 ${Math.max(9, fontSize-2)}px 'Noto Sans TC'`;
222
+ ctx.fillText('萬物皆數 fb.com/groups/...', w - 16, h - 14 - (fontSize + 6));
223
+ }
224
+
225
+ // ---- 點擊 canvas → 彈出詞彙選單(避免命中問題) ----
226
+ canvas.addEventListener('click', () => {
227
+ if (Object.keys(words).length === 0) return;
228
+ const listEl = document.getElementById('pick-list');
229
+ listEl.innerHTML = '';
230
+ Object.keys(words).sort((a,b) => words[b] - words[a]).forEach(word => {
231
+ const chip = document.createElement('span');
232
+ chip.className = 'word-chip';
233
+ chip.textContent = word;
234
+ chip.addEventListener('click', () => { hidePick(); showZoom(word); });
235
+ listEl.appendChild(chip);
236
+ });
237
+ document.getElementById('pick-overlay').style.display = 'flex';
238
+ });
239
+ function hidePick(){ document.getElementById('pick-overlay').style.display = 'none'; }
240
+
241
+ // ---- 放大顯示 ----
242
+ function showZoom(word) {
243
+ selectedWord = word;
244
+ document.getElementById('zoom-text').textContent = word;
245
+ document.getElementById('zoom-overlay').style.display = 'flex';
246
+ }
247
+ function hideZoom(){ document.getElementById('zoom-overlay').style.display = 'none'; selectedWord = null; }
248
+ document.getElementById('zoom-close').addEventListener('click', hideZoom);
249
+ document.getElementById('zoom-overlay').addEventListener('click', e => {
250
+ if (e.target === document.getElementById('zoom-overlay')) hideZoom();
251
+ });
252
+ document.getElementById('zoom-delete').addEventListener('click', async () => {
253
+ if (!selectedWord) return;
254
+ await updateDoc(roomRef, { words: { [selectedWord]: deleteField() } });
255
+ hideZoom();
256
+ });
257
+
258
+ // ---- 控制 ----
259
+ document.getElementById('btn-lock').addEventListener('click', async () => {
260
+ isLocked = !isLocked;
261
+ await updateDoc(roomRef, { locked: isLocked });
262
+ });
263
+ document.getElementById('btn-multiple').addEventListener('click', async () => {
264
+ allowMultiple = !allowMultiple;
265
+ await updateDoc(roomRef, { allowMultiple });
266
+ });
267
+ document.getElementById('q-send').addEventListener('click', async () => {
268
+ const text = document.getElementById('q-input').value.trim();
269
+ await updateDoc(roomRef, { question: text });
270
+ });
271
+ document.getElementById('btn-fullscreen').addEventListener('click', () => {
272
+ if (!document.fullscreenElement) document.documentElement.requestFullscreen();
273
+ else document.exitFullscreen();
274
+ });
275
+ document.getElementById('btn-screenshot').addEventListener('click', () => {
276
+ const temp = document.createElement('canvas');
277
+ temp.width = canvas.width; temp.height = canvas.height;
278
+ const t = temp.getContext('2d');
279
+ t.fillStyle = '#F8FAFC'; t.fillRect(0,0,temp.width,temp.height);
280
+ t.drawImage(canvas, 0, 0);
281
+ const a = document.createElement('a');
282
+ a.href = temp.toDataURL('image/png');
283
+ a.download = `即時互動文字雲_${roomCode}.png`;
284
+ a.click();
285
+ });
286
+ document.getElementById('btn-csv').addEventListener('click', () => {
287
+ const rows = [['詞彙','次數'], ...Object.entries(words)];
288
+ const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g,'""')}"`).join(',')).join('\n');
289
+ const blob = new Blob(['\uFEFF'+csv], { type:'text/csv;charset=utf-8;' });
290
+ const u = URL.createObjectURL(blob);
291
+ const a = document.createElement('a'); a.href = u; a.download = `文字雲_${roomCode}.csv`; a.click();
292
+ URL.revokeObjectURL(u);
293
+ });
294
+ document.getElementById('btn-reset').addEventListener('click', async () => {
295
+ if (confirm('確定清除所有詞彙並重新開始?')) await updateDoc(roomRef, { words: {} });
296
+ });
297
+
298
+ // ---- QR Code ----
299
+ import('https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js').then(() => {
300
+ const joinUrl = `${location.origin}${location.pathname.replace(/[^/]*$/, '')}guest.html?room=${roomCode}`;
301
+ document.getElementById('join-link').textContent = joinUrl;
302
+ new QRCode(document.getElementById('qr'), { text: joinUrl, width: 90, height: 90 });
303
+ });
304
+ </script>
305
+
306
+ <script>
307
+ window.MathJax = {
308
+ tex: { inlineMath: [['$','$'],['\\(','\\)']], displayMath: [['$$','$$'],['\\[','\\]']], processEscapes: true },
309
+ options: { skipHtmlTags: ['script','noscript','style','textarea','pre'] }
310
+ };
311
+ </script>
312
+ <script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
313
+
314
+ </body>
315
+ </html>