Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
grep -q 'POLL_CFG={"sim"' docs/index.html && ! grep -q '"key":' docs/index.html # Live Demo 必須是模擬模式、不含 key
for f in skills/html-slide-deck/references/live-poll/*.mjs skills/html-slide-deck/references/live-poll/screen/*.js skills/html-slide-deck/references/live-poll/worker/src/*.js; do node --check "$f"; done
node -e "JSON.parse(require('fs').readFileSync('skills/html-slide-deck/references/live-poll/worker/src/polls.json','utf8'))"
(cd skills/html-slide-deck/references/live-poll/worker && node --test test/stats.test.js)
python -m py_compile skills/html-slide-deck/scripts/montage.py examples/pptx-edits-back-to-html.py
bash -n install.sh skills/html-slide-deck/scripts/*.sh
pip install ruff && ruff check skills/html-slide-deck/scripts/montage.py examples/pptx-edits-back-to-html.py
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- 手機投票新增**模擬模式**:`poll-slides.mjs --polls <polls.json> --sim [人數]`,不連後端、不含 key,可公開;`live-poll.js` 依 `POLL_CFG.sim` 自動跑出作答過程。
- 新增 `README.en.md`(英文完整說明)、`SECURITY.md`(漏洞回報、ADMIN_KEY 處理與輪替、QA 會執行 JS);README 開頭放 Live Demo 連結與 45 秒流程 GIF(`docs/demo.gif`)。
- 乾淨環境安裝測試(OrbStack Ubuntu 24.04,照 README 從 clone 到離線 QA、講義、PDF、做法 B 全程):發現 **Node 18 跑不了 Playwright**,需求改為 Node 20+(README 徽章、install.sh 檢查、SKILL.md、engines),疑難排解補上;`build-handout.mjs` 找不到 render 目錄時改成明確錯誤訊息。
- 修正 PR #3/#4 的 Codex 審查意見:數字題中位數偶數筆改取中間兩筆平均(抽出 `worker/src/stats.js` 並加 `node --test` 測試,CI 一併執行);投影片端快速換頁不再重開已離開的題目(控制請求排隊+換頁序號);手機頁回前景時不會多開一條 WebSocket;模擬模式開在加入頁時就顯示模擬人數;`qrcode` 改為 skill 自帶套件,照文件安裝即可跑 `poll-slides.mjs`;README 部署指令改用子 shell,不會停在 `my-poll` 裡;Live Demo 安裝頁改為 Node 20+。

## 1.3.0 — 2026-10-18

Expand Down
4 changes: 2 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ Used by 60 people at once at a medical conference talk in October 2026, and load
<section data-poll="warmup"></section> <!-- one question per slide, defined in worker/src/polls.json -->
```
```bash
cp -R $SKILL/references/live-poll/worker my-poll && cd my-poll && npm install
npx wrangler login && npx wrangler secret put ADMIN_KEY && npx wrangler deploy # put the same key in .dev.vars
cp -R $SKILL/references/live-poll/worker my-poll
(cd my-poll && npm install && npx wrangler login && npx wrangler secret put ADMIN_KEY && npx wrangler deploy) # put the same key in my-poll/.dev.vars
node $SKILL/scripts/poll-slides.mjs index.html index-live.html --worker my-poll --url https://slide-poll.<subdomain>.workers.dev
```

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ QA:qa-screenshots.mjs ─ 截圖、溢出、JS 錯誤、對外請求、QR 逐
<section data-poll="warmup"></section> <!-- 一題一頁,題目定義在 worker/src/polls.json -->
```
```bash
cp -R $SKILL/references/live-poll/worker my-poll && cd my-poll && npm install
npx wrangler login && npx wrangler secret put ADMIN_KEY && npx wrangler deploy # 同一把 key 也寫進 .dev.vars
cp -R $SKILL/references/live-poll/worker my-poll
(cd my-poll && npm install && npx wrangler login && npx wrangler secret put ADMIN_KEY && npx wrangler deploy) # 同一把 key 也寫進 my-poll/.dev.vars
node $SKILL/scripts/poll-slides.mjs index.html index-live.html --worker my-poll --url https://slide-poll.<子網域>.workers.dev
```

Expand Down
2 changes: 1 addition & 1 deletion demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ <h2>30 秒安裝</h2>

<span class="c"># 重開 CLI 後直接說</span>
「做一份 HTML 簡報」</div>
<div class="note">支援 <b>Claude Code、Codex CLI、Gemini CLI、Grok Build</b>。需要 Node 18+;QA 用 Playwright,講義用 Python。</div>
<div class="note">支援 <b>Claude Code、Codex CLI、Gemini CLI、Grok Build</b>。需要 Node 20+;QA 用 Playwright,講義用 Python。</div>
</div>
<div class="qrcard" data-url="https://github.com/keanu77/htmlpptskill">__QR__<div class="qrl">github.com/keanu77/htmlpptskill</div></div>
</div>
Expand Down
38 changes: 21 additions & 17 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ <h2>30 秒安裝</h2>

<span class="c"># 重開 CLI 後直接說</span>
「做一份 HTML 簡報」</div>
<div class="note">支援 <b>Claude Code、Codex CLI、Gemini CLI、Grok Build</b>。需要 Node 18+;QA 用 Playwright,講義用 Python。</div>
<div class="note">支援 <b>Claude Code、Codex CLI、Gemini CLI、Grok Build</b>。需要 Node 20+;QA 用 Playwright,講義用 Python。</div>
</div>
<div class="qrcard" data-url="https://github.com/keanu77/htmlpptskill"><svg class="qrsvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31 31" shape-rendering="crispEdges"><path fill="#FFFFFF" d="M0 0h31v31H0z"/><path stroke="#0B1F2A" d="M1 1.5h7m2 0h1m1 0h1m1 0h1m2 0h5m1 0h7M1 2.5h1m5 0h1m3 0h2m3 0h3m2 0h1m1 0h1m5 0h1M1 3.5h1m1 0h3m1 0h1m1 0h2m1 0h1m2 0h1m3 0h1m3 0h1m1 0h3m1 0h1M1 4.5h1m1 0h3m1 0h1m1 0h2m3 0h2m2 0h1m4 0h1m1 0h3m1 0h1M1 5.5h1m1 0h3m1 0h1m1 0h1m2 0h3m2 0h1m2 0h2m1 0h1m1 0h3m1 0h1M1 6.5h1m5 0h1m1 0h1m2 0h1m2 0h3m3 0h1m1 0h1m5 0h1M1 7.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M9 8.5h4m8 0h1M1 9.5h1m1 0h5m2 0h1m2 0h7m3 0h5M2 10.5h1m5 0h2m1 0h1m2 0h1m2 0h1m2 0h2m1 0h3m3 0h1M6 11.5h2m4 0h2m1 0h2m1 0h1m2 0h1m3 0h1M1 12.5h1m1 0h2m1 0h1m2 0h1m1 0h2m3 0h2m2 0h1m5 0h1m1 0h1M2 13.5h1m4 0h1m1 0h1m6 0h5m5 0h2M1 14.5h1m1 0h2m3 0h2m1 0h2m1 0h1m2 0h1m2 0h6m3 0h1M1 15.5h1m2 0h1m1 0h2m1 0h2m1 0h5m1 0h1m2 0h2m2 0h3M5 16.5h2m1 0h1m3 0h3m1 0h1m3 0h6m2 0h1M1 17.5h3m1 0h4m3 0h2m1 0h6m1 0h1m1 0h1m1 0h2M1 18.5h4m1 0h1m4 0h2m4 0h4m1 0h4m1 0h1m1 0h1M1 19.5h1m2 0h2m1 0h1m1 0h4m3 0h1m1 0h1m2 0h1m1 0h3m1 0h1M1 20.5h1m1 0h2m1 0h1m8 0h2m4 0h3m4 0h1M1 21.5h1m3 0h1m1 0h3m3 0h1m2 0h3m1 0h6m1 0h3M9 22.5h1m3 0h3m1 0h3m1 0h1m3 0h5M1 23.5h7m2 0h1m2 0h4m2 0h3m1 0h1m1 0h3M1 24.5h1m5 0h1m1 0h2m1 0h1m1 0h1m1 0h2m1 0h3m3 0h1m3 0h1M1 25.5h1m1 0h3m1 0h1m1 0h1m1 0h3m1 0h1m2 0h1m2 0h5m1 0h1M1 26.5h1m1 0h3m1 0h1m1 0h4m4 0h1m2 0h2m4 0h4M1 27.5h1m1 0h3m1 0h1m1 0h1m3 0h1m2 0h2m2 0h9M1 28.5h1m5 0h1m2 0h1m2 0h1m2 0h2m1 0h1m1 0h2m1 0h1m1 0h1m1 0h1M1 29.5h7m1 0h5m4 0h1m1 0h1m2 0h3m1 0h1"/></svg>
<div class="qrl">github.com/keanu77/htmlpptskill</div></div>
Expand Down Expand Up @@ -818,7 +818,7 @@ <h1 data-letters>謝謝觀看</h1>
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const pollSec = id => document.querySelector(`section[data-poll="${id}"]`);
const results = {};
let current = null, ws = null, wsRetry = 0, poller = null;
let current = null, ws = null, wsRetry = 0, poller = null, navSeq = 0;

/* ---------------------------------------------------------------- 模擬模式 */
const SIM = CFG.sim ? (CFG.sim === true ? 60 : CFG.sim) : 0;
Expand All @@ -831,7 +831,8 @@ <h1 data-letters>謝謝觀看</h1>
} else {
const sorted = [...v].sort((a, b) => a - b);
out.counts = def.bins.map(([lo, hi]) => v.filter(x => x >= lo && x <= hi).length);
out.median = sorted.length ? sorted[Math.floor((sorted.length - 1) / 2)] : null;
const mid = sorted.length >> 1; // 偶數筆取中間兩筆平均
out.median = !sorted.length ? null : sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
out.close = v.filter(x => Math.abs(x - def.answer) <= def.answer * 0.2).length;
}
return out;
Expand All @@ -849,7 +850,6 @@ <h1 data-letters>謝謝觀看</h1>
const def = CFG.polls[id];
if (sims[id]) return render(id);
sims[id] = { votes: [], timer: null };
$$('.lp-phones').forEach(el => { el.textContent = SIM; });
sims[id].timer = setInterval(() => {
const s = sims[id];
for (let k = 0; k < 1 + Math.floor(Math.random() * 4) && s.votes.length < SIM; k++) s.votes.push(simVote(def));
Expand All @@ -874,10 +874,14 @@ <h1 data-letters>謝謝觀看</h1>
return await r.json();
} finally { clearTimeout(timer); }
}
const control = body => api('/api/control', {
method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer ' + CFG.key },
body: JSON.stringify(body),
});
// 控制請求排隊依序送出,快速換頁時「開題」不會晚於「離開」抵達後端。
// 換頁指令帶 seq:輪到時若已換過頁就略過(回傳 null),斷線後只送最新狀態,不會重播積壓的舊題。
let ctlChain = Promise.resolve();
const control = (body, seq) => (ctlChain = ctlChain.catch(() => {}).then(() =>
seq !== undefined && seq !== navSeq ? null : api('/api/control', {
method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer ' + CFG.key },
body: JSON.stringify(body),
})));

function setNet(ok, msg) {
document.documentElement.classList.toggle('lp-live', ok);
Expand Down Expand Up @@ -960,18 +964,18 @@ <h1 data-letters>謝謝觀看</h1>
$$('.ok', sec).forEach(el => el.classList.remove('ok'));
}

async function enterPoll(id) {
async function enterPoll(id, seq) {
current = id;
if (SIM) return simStart(id);
try {
const res = await api('/api/results?poll=' + id);
results[id] = res;
if (seq !== navSeq) return; // 等待期間已換頁:不要再開這題
if (res.phase !== 'revealed') {
const r = await control({ poll: id, phase: 'open' });
const r = await control({ poll: id, phase: 'open' }, seq);
if (!r) return; // 排隊期間已換頁
results[id] = r.results;
} else {
await control({ poll: id, phase: 'revealed' }); // 回到已揭曉的題:讓手機也顯示答案
}
} else if (!await control({ poll: id, phase: 'revealed' }, seq)) return; // 回到已揭曉的題:讓手機也顯示答案
render(id);
} catch (e) {
setNet(false, '連不到投票伺服器(按 A 仍可揭曉)');
Expand All @@ -994,9 +998,9 @@ <h1 data-letters>謝謝觀看</h1>
}

function onShow(sec) {
const id = sec && sec.dataset.poll;
if (id) return enterPoll(id);
if (current) { current = null; if (!SIM) control({ poll: null }).catch(() => {}); }
const id = sec && sec.dataset.poll, seq = ++navSeq;
if (id) return enterPoll(id, seq);
if (current) { current = null; if (!SIM) control({ poll: null }, seq).catch(() => {}); }
}

// capture 階段攔截,避免和 reveal.js 內建鍵衝突;只在投票頁生效
Expand All @@ -1007,7 +1011,7 @@ <h1 data-letters>謝謝觀看</h1>
}, true);

function start() {
if (SIM) setNet(true);
if (SIM) { setNet(true); $$('.lp-phones').forEach(el => { el.textContent = SIM; }); }
else if ($$('section[data-poll], .lp-joinslide').length) connect();
Reveal.on('slidechanged', e => onShow(e.currentSlide));
onShow(Reveal.getCurrentSlide());
Expand Down
Loading
Loading