diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a03bbe..97db8c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: run: | for f in skills/html-slide-deck/scripts/*.mjs skills/html-slide-deck/references/*.mjs skills/html-slide-deck/references/offline-build-template/scripts/*.mjs; do node --check "$f"; done node --check skills/html-slide-deck/references/offline-build-template/build.js + node --check demo/build.mjs + 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'))" python -m py_compile skills/html-slide-deck/scripts/montage.py examples/pptx-edits-back-to-html.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b57719a..757f27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +- **Live Demo**(GitHub Pages):`demo/` 原始檔 → `docs/index.html`,12 頁示範逐步出現、Auto-Animate、3D 翻卡、頁內測驗、手機投票、滑桿對比;build 時量實際檔案大小當猜數字題的答案。 +- 手機投票新增**模擬模式**:`poll-slides.mjs --polls --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 目錄時改成明確錯誤訊息。 + ## 1.3.0 — 2026-10-18 - 新增觀眾手機即時投票 `skills/html-slide-deck/references/live-poll/`:Cloudflare Worker+Durable Object 後端(手機頁、題目 JSON、WebSocket 推播、斷線輪詢)、投影片端 `screen/live-poll.js`/`.css`、`load-test.mjs`(150 支手機壓測);新腳本 `scripts/poll-slides.mjs` 把占位 section 換成加入頁與投票頁並注入講者端程式。源自 2026-10 學會演講的做法(現場 60 人使用)。 diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..8139109 --- /dev/null +++ b/README.en.md @@ -0,0 +1,162 @@ +
+ +# htmlpptskill + +**Tell your AI coding agent "make me an HTML slide deck" and get a deck that plays offline, has every QR code verified, lets the audience vote from their phones, and ships with handout PPTX/PDF.** + +Two Agent Skills (open `SKILL.md` format) for Claude Code, Codex CLI, Gemini CLI and Grok Build. + +**[▶ Live Demo](https://keanu77.github.io/htmlpptskill/)** · [中文 README](README.md) · [Security](SECURITY.md) · [Changelog](CHANGELOG.md) + +One prompt, then the deck: fragments, Auto-Animate, 3D flip cards, an in-slide quiz, a live audience poll, screenshot QA and three deliverables + +
+ +> The docs and slide templates are in Traditional Chinese, but the scripts are language-neutral: you can write your slides in any language. + +## Why not just PowerPoint? + +| Problem | What this repo does | +|---|---| +| No Wi-Fi at the venue: fonts, images and plugins break | `QA_OFFLINE=1` fails on any external request; reveal.js, images and QR codes are inlined into **one HTML file** | +| A misprinted QR code sends the audience to a 404 | QA screenshots **every QR code, decodes it** and compares it with `data-url`; a mismatch exits 1 | +| AI-generated slides overflow or miss speaker notes | Every slide is screenshotted into one `montage.png`; overflow, JS errors and missing notes are listed | +| The organizer wants PPTX, you only have HTML | `render-slides` → `build-handout`: one image per slide, speaker notes in the notes pane; PDF in one command | +| You want the whole room to answer, not one volunteer | **Live audience polling**: scan a QR, vote on the phone, results stream to the slide, press `A` to reveal on every phone | +| The speaker edited the PPTX by hand | A full worked example of pulling the edits back into HTML | + +## The two skills + +| Skill | What it does | Triggers | +|---|---|---| +| **`html-slide-deck`** | Outline → HTML → QA → delivery: layouts, images, QR codes, interactive pages, live polling, handout PPTX/PDF, native editable PPTX | "make an HTML deck", "reveal.js", "add QR codes", "live poll", "export to PPT/PDF", "offline deck" | +| **`html-slide-effects`** | Fragments, Auto-Animate, 3D flip cards, GSAP text, timer bar, lightbox, scroll view, with a STATIC switch for QA and PDF | "add animations", "flip cards", "make it more engaging" | + +## Install (30 seconds) + +```bash +git clone https://github.com/keanu77/htmlpptskill.git && cd htmlpptskill +./install.sh # installs into every CLI it detects; existing skills are moved to skills-backup/ +``` + +| CLI | Installed to | How to use | +|---|---|---| +| **Claude Code** | `~/.claude/skills/` | Type `/html-slide-deck`, or just ask for an HTML deck. As a plugin: `/plugin marketplace add keanu77/htmlpptskill` → `/plugin install htmlpptskill@keanu77` | +| **Codex CLI** | `~/.codex/skills/` | Describe the task, or `$html-slide-deck` | +| **Gemini CLI** | `~/.gemini/skills/` | `gemini skills list`; or `gemini skills link /skills/html-slide-deck` | +| **Grok Build** | `~/.grok/skills/` | Describe the task (it also reads `~/.claude/skills/`) | + +One CLI only: `./install.sh codex`. Commit with your project: `./install.sh --project` (installs to `.agents/skills/`). + +### Requirements + +| Needed for | Install | +|---|---| +| All scripts | Node 20+ (Playwright does not support 18; Ubuntu 24.04's `apt install nodejs` is 18, use nvm or nodejs.org). Node 22+ for the poll load test | +| Screenshot QA, rendering, PDF | `npm i -D playwright && npx playwright install chromium` (Linux servers: also `npx playwright install-deps chromium`) | +| Handout PPTX | `npm i pptxgenjs` | +| QR generation, montage | Python 3 + `pip install -r requirements.txt` (segno, pillow, python-pptx; a venv is recommended) | +| CJK text on Linux | A CJK font, e.g. `fonts-noto-cjk` (otherwise screenshots show boxes) | +| Live polling (optional) | A free Cloudflare account | + +Scripts look for `node_modules` **from the working directory upwards**, then in the global npm root; or set `SKILL_PKG_ROOT`. + +| Platform | Support | +|---|---| +| macOS | Everything (`pptx-to-pdf.sh` needs Microsoft PowerPoint) | +| Linux | Everything except the native PowerPoint export (use `print-pdf.mjs`) | +| Windows | Run the scripts with `node`; use WSL for `install.sh` or copy `skills\*` manually; set `PYTHON=python` | + +## First deck in 10 minutes + +```bash +mkdir my-deck && cd my-deck && npm init -y +npm i -D playwright && npx playwright install chromium && npm i reveal.js@5 pptxgenjs +SKILL=~/.claude/skills/html-slide-deck # your CLI's skill path + +cp $SKILL/references/reveal-template.html draft.html # 9-slide template; 〈…〉 and example.com are placeholders +node $SKILL/scripts/inline-assets.mjs draft.html index.html # inline reveal.js and images → one offline file +QA_OFFLINE=1 node $SKILL/scripts/qa-screenshots.mjs index.html qa/ # screenshots, QR, overflow → look at qa/montage.png +node $SKILL/scripts/render-slides.mjs index.html render/ +node $SKILL/scripts/build-handout.mjs render/ handout.pptx "Speaker" # image handout with notes +node $SKILL/scripts/print-pdf.mjs index.html handout.pdf # PDF handout +``` + +Or just tell your agent: + +> Use html-slide-deck to make a 20-slide deck on "XXX" for residents. No Wi-Fi at the venue; put a QR code on every tool slide. + +It proposes an outline table first and waits for your OK, then writes the HTML, runs QA and delivers the files. + +## Live audience polling + +Used by 60 people at once at a medical conference talk in October 2026, and load-tested beforehand with 150 simulated phones (150 votes written and pushed to the slide in about 0.2–1.2 s). + +- The audience scans a QR code: no app, no login, anonymous, answers can be changed until the reveal. +- The slide shows live percentages (multiple choice) or a histogram and median (guess-a-number), plus how many phones are connected. +- Press `A` to reveal: the slide marks the answer and every phone shows it. `Shift+R` clears the votes for that question. +- If the venue network drops, the slide shows a warning, falls back to polling, and `A` still reveals. +- Backend: a Cloudflare Worker + Durable Object (free plan). Deploy it once and then edit only `polls.json`. + +```html +
+
+``` +```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 +node $SKILL/scripts/poll-slides.mjs index.html index-live.html --worker my-poll --url https://slide-poll..workers.dev +``` + +> **The presenter file `index-live.html` contains the ADMIN_KEY.** Keep it on the presenter's computer only: never publish, email or commit it. See [SECURITY.md](SECURITY.md). + +For a public demo, use simulation mode (no backend, no key, safe to publish). The Live Demo is built this way: + +```bash +node $SKILL/scripts/poll-slides.mjs index.html demo.html --polls polls.json --sim 60 +``` + +Full setup, question format, API and load test: [`live-poll/README.md`](skills/html-slide-deck/references/live-poll/README.md) (Chinese). + +## Shortcuts, transitions and effects + +While presenting: `→`/`Space` next, `S` speaker view, `O`/`Esc` overview, `F` fullscreen, `B` black screen, `G` go to slide, `A` reveal (poll slides), `?` all shortcuts. Append `?print-pdf` to print, `?view=scroll` to read on a phone. + +All effects come with copy-paste code in [`docs/TECHNIQUES.md`](docs/TECHNIQUES.md) (Chinese): fragments, Auto-Animate, 3D flip cards, card-wall fly-in, GSAP letter-by-letter text, Three.js cover particles, spotlight, timer bar, lightbox, in-slide quiz, live polling, interactive map, and a STATIC switch so QA screenshots and PDFs capture the final state. + +## Rules learned the hard way + +- **Agree on the outline before any HTML.** Regenerating 50 slides costs ten times more than aligning an outline. +- **QA is mandatory, and you must look at the images.** Open `montage.png` first, then the flagged slides; verify again after fixing. +- **Every QR code needs a `data-url`.** Without it QA cannot compare; placeholder URLs (example.com) trigger warnings. +- **Assume the venue has no internet.** Run `QA_OFFLINE=1` before delivery; use system fonts instead of Google Fonts. +- **Ask before deploying or creating a repo.** The skills never push anything to the public web on their own. +- **The QA scripts execute the deck's JavaScript.** Run them only on HTML you generated. + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| `Playwright requires Node.js 20 or higher` | Upgrade Node (nvm or nodejs.org); Ubuntu 24.04's apt package is 18 | +| `找不到套件 playwright` (package not found) | Run from a directory with `node_modules`, or `npm i -g playwright`, or set `SKILL_PKG_ROOT` | +| `缺 jsqr/pngjs` | Run `npm install` in the skill directory, or set `QA_SKIP_QR=1` explicitly | +| Chromium not downloaded | `npx playwright install chromium`; or point `CHROMIUM_PATH` at an existing Chromium/Chrome | +| Chinese text renders as boxes (Linux) | Install Noto Sans CJK | +| Sites like PubMed return 403 to headless browsers | Ask the user for a screenshot | +| PDF has more pages than slides | `pdfSeparateFragments: false` in `Reveal.initialize` (already set in the templates) | +| Poll votes stay at 0 in your own tests | Voter IDs must be 8–64 characters `[a-z0-9-]` | +| `wrangler dev` will not start | `compatibility_date` is newer than your local workerd; move it back | + +## Repository layout + +``` +skills/html-slide-deck/ main workflow: SKILL.md, scripts/, references/ (templates, live-poll/, pitfalls, design principles) +skills/html-slide-effects/ effects skill +demo/ Live Demo source → docs/index.html (GitHub Pages) +examples/ a 50-slide workshop deck, PPTX-edits-back-to-HTML example, Three.js particles +docs/ TECHNIQUES.md, images, Live Demo build +``` + +## About + +The author is a sports medicine physician who built these skills while preparing workshop and conference talks, then had four models (Codex, Claude, Grok, Gemini) review them. MIT licensed. The template text, URLs and screenshots in `examples/` are samples to replace. diff --git a/README.md b/README.md index a5bf193..8ee22ad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,13 @@ # htmlpptskill -**跟 AI 說一句「做一份 HTML 簡報」,拿到能離線播、QR 驗過、還附講義 PPTX/PDF 的成品。** +**跟 AI 說一句「做一份 HTML 簡報」,拿到能離線播、QR 驗過、觀眾能用手機投票、還附講義 PPTX/PDF 的成品。** + +### [▶ 打開 Live Demo,直接操作](https://keanu77.github.io/htmlpptskill/) + +這份 12 頁的 Demo 就是用這個 skill 做的:逐步出現、Auto-Animate、3D 翻卡、頁內測驗、手機投票(模擬 60 人)、滑桿對比。279 KB 單檔,電腦按 → 翻頁。 + +[English README](README.en.md) · [安全說明](SECURITY.md) reveal.js 簡報製作流程,包成兩個 Agent Skill。
Claude Code・Codex CLI・Gemini CLI・Grok Build 都能裝。
@@ -10,12 +16,16 @@ Claude Code・Codex CLI・Gemini CLI・Grok Build 都能裝。
[![CI](https://github.com/keanu77/htmlpptskill/actions/workflows/ci.yml/badge.svg)](https://github.com/keanu77/htmlpptskill/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](LICENSE) -![Node 18+](https://img.shields.io/badge/Node-18%2B-3FC1B7) +![Node 20+](https://img.shields.io/badge/Node-20%2B-3FC1B7) ![Agent Skills](https://img.shields.io/badge/Agent%20Skills-SKILL.md-F2B84B) +一句話 → Demo 操作 → 逐頁 QA → HTML、PDF、PPTX 三種交付 + +45 秒看完流程:一句話、逐步出現、Auto-Animate、翻卡、測驗、手機投票、QA、三種交付。 + 封面逐字浮現、3D 翻卡、頁內可作答的測驗、雙 QR 工具頁 -實際講座成品:封面 GSAP 逐字浮現、七張卡片一次翻面、頁內真的能作答的測驗、網站+repo 雙 QR。54 頁、3 MB、拔掉網路照樣播。 +另一份實際講座成品:封面 GSAP 逐字浮現、七張卡片一次翻面、頁內真的能作答的測驗、網站+repo 雙 QR。54 頁、3 MB、拔掉網路照樣播。

安裝 · 10 分鐘上手 · 現場互動 · 快捷鍵、換場與動畫技巧 · 疑難排解 @@ -151,7 +161,13 @@ npx wrangler login && npx wrangler secret put ADMIN_KEY && npx wrangler deploy node $SKILL/scripts/poll-slides.mjs index.html index-live.html --worker my-poll --url https://slide-poll.<子網域>.workers.dev ``` -產出的 `index-live.html` 內含講者控制用的 ADMIN_KEY,**只放講者電腦**;要分享的版本用原本的 `index.html`。 +產出的 `index-live.html` 內含講者控制用的 ADMIN_KEY,**只放講者電腦**;要分享的版本用原本的 `index.html`,詳見 [SECURITY.md](SECURITY.md)。 + +要公開展示(像 Live Demo)就用模擬模式:不連後端、不含 key,進到投票頁自動跑出作答過程,`A` 一樣能揭曉。 + +```bash +node $SKILL/scripts/poll-slides.mjs index.html demo.html --polls polls.json --sim 60 +``` ### 互動地圖與製作技巧頁 @@ -634,6 +650,9 @@ examples/ workshop-2026/content.js 50 頁工作坊完整內容 pptx-edits-back-to-html.py 講者在 PPTX 手改後回灌 HTML+全部效果的實例(見 examples/README.md) docs/TECHNIQUES.md 快捷鍵、換場、動畫技巧參考(附程式碼) +docs/index.html Live Demo 成品(GitHub Pages;原始檔在 demo/,cd demo && npm i && npm run build) +demo/ Live Demo 原始檔:src/index.html、polls.json、build.mjs +README.en.md、SECURITY.md 英文說明、安全說明(ADMIN_KEY、QA 會執行 JS、漏洞回報) docs/html-slides-tips-from-x-2026-10-17.md X 使用者分享的 HTML 簡報技巧與工具整理(含來源連結) docs/*.png 成果圖 .claude-plugin/ Claude Code plugin/marketplace 設定 @@ -644,6 +663,7 @@ AGENTS.md、CLAUDE.md 給在本 repo 工作的 agent | 現象 | 處理 | |---|---| +| `Playwright requires Node.js 20 or higher` | Node 太舊。Ubuntu 24.04 的 `apt install nodejs` 是 18,改用 nvm 或 nodejs.org 的 Node 20+ | | `找不到套件 playwright` | 在有 `node_modules` 的專案目錄執行,或 `npm i -g playwright`,或設 `SKILL_PKG_ROOT` | | `缺 jsqr/pngjs` | 到 skill 目錄 `npm install`;或 `QA_SKIP_QR=1` 明確略過 | | Chromium 沒下載 | `npx playwright install chromium`;已有 Chromium 就設 `CHROMIUM_PATH` | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6d55b1d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# 安全說明 / Security + +## 回報漏洞 / Reporting a vulnerability + +請用 GitHub 的 [Private vulnerability reporting](https://github.com/keanu77/htmlpptskill/security/advisories/new) 私下回報,不要開公開 issue。請附:受影響的檔案或腳本、重現步驟、影響範圍。 +Please report privately via GitHub's private vulnerability reporting, not a public issue. Include the affected file or script, steps to reproduce, and impact. + +## 手機即時投票的 ADMIN_KEY / The live-poll ADMIN_KEY + +`scripts/poll-slides.mjs` 產生的**講者版 HTML 內含 ADMIN_KEY**。拿到這把 key 的人可以開題、關題、清空所有票數。 +The presenter HTML produced by `scripts/poll-slides.mjs` **contains the ADMIN_KEY**. Anyone holding it can open, close and wipe every poll. + +- 講者版只放講者電腦:**不要上傳到網站、GitHub Pages、雲端硬碟,不要寄出、不要 commit。** + Keep the presenter file on the presenter's computer only: never publish it, put it on GitHub Pages or a shared drive, email it, or commit it. +- 要分享給觀眾或公開的版本,用沒跑過 `poll-slides.mjs` 的原始 HTML,或用模擬模式 `--sim`(不連後端、不含 key)。 + To share or publish, use the original HTML, or simulation mode `--sim` (no backend, no key). +- key 存在 `worker/.dev.vars`(已 gitignore)與 Cloudflare secret(`wrangler secret put ADMIN_KEY`),不要寫進原始碼。 + The key lives in `worker/.dev.vars` (gitignored) and a Cloudflare secret, never in source code. +- commit 前檢查 staged 內容沒有 key:`git diff --cached | grep -c "<你的 key>"` 應為 0。 + Before committing, check the staged diff does not contain the key. +- 懷疑外流就換一把:`openssl rand -hex 24`,重新 `wrangler secret put ADMIN_KEY`、更新 `.dev.vars`,再重新產生講者版。舊檔裡的 key 立即失效。 + If it may have leaked, rotate it and regenerate the presenter file; the old key stops working immediately. + +投票後端的其他設計:手機頁與 `/api/state` 在揭曉前不含答案;投票的 voter 代號限 8–64 字元英數;request body 上限 2 KB;控制 API 需要 `Authorization: Bearer`。觀眾匿名作答,不收集個人資料。 +Other backend properties: answers are never sent before the reveal; voter IDs are restricted to 8–64 alphanumeric characters; request bodies are capped at 2 KB; the control API requires a bearer token; voting is anonymous. + +## QA 腳本會執行簡報裡的 JavaScript / QA scripts run the deck's JavaScript + +`qa-screenshots.mjs`、`render-slides.mjs`、`print-pdf.mjs` 用 Chromium 開啟並執行簡報。只對自己產生或審過的 HTML 跑,不要拿來檢查來路不明的檔案。 +These scripts open and execute the deck in Chromium. Only run them on HTML you generated or reviewed. + +## 部署 / Deployment + +兩個 skill 都不會自己部署或開公開 repo;agent 在部署、建立帳號或推到公網前必須先問使用者。 +Neither skill deploys or creates public repositories on its own; the agent must ask before deploying, creating accounts or publishing. diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..8d6c652 --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +qa-out/ diff --git a/demo/build.mjs b/demo/build.mjs new file mode 100644 index 0000000..ccc68b9 --- /dev/null +++ b/demo/build.mjs @@ -0,0 +1,31 @@ +// Live Demo build:src/index.html → 內嵌 QR → inline-assets(reveal.js 內嵌)→ poll-slides --sim(模擬投票)→ ../docs/index.html +// 「這份 Demo 有多大」那題的答案在 build 時量實際檔案大小。 +import { readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import QR from 'qrcode'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SCRIPTS = join(HERE, '..', 'skills', 'html-slide-deck', 'scripts'); +const BUILD = join(HERE, 'build'), OUT = join(HERE, '..', 'docs', 'index.html'); +mkdirSync(BUILD, { recursive: true }); +const run = (script, ...args) => execFileSync(process.execPath, [join(SCRIPTS, script), ...args], { cwd: HERE, stdio: 'inherit' }); + +const svg = await QR.toString('https://github.com/keanu77/htmlpptskill', + { type: 'svg', margin: 1, errorCorrectionLevel: 'M', color: { dark: '#0B1F2A', light: '#FFFFFF' } }); +writeFileSync(join(BUILD, 'qr.html'), readFileSync(join(HERE, 'src', 'index.html'), 'utf8') + .replace('__QR__', svg.replace('=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/reveal.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/reveal.js/-/reveal.js-5.2.1.tgz", + "integrity": "sha512-r7//6mIM5p34hFiDMvYfXgyjXqGRta+/psd9YtytsgRlrpRzFv4RbH76TXd2qD+7ZPZEbpBDhdRhJaFgfQ7zNQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000..15ad91b --- /dev/null +++ b/demo/package.json @@ -0,0 +1,17 @@ +{ + "name": "htmlpptskill-demo", + "private": true, + "type": "module", + "description": "Live Demo:build 到 ../docs/index.html(GitHub Pages)", + "scripts": { + "build": "node build.mjs", + "qa": "QA_OFFLINE=1 node ../skills/html-slide-deck/scripts/qa-screenshots.mjs ../docs/index.html qa-out" + }, + "dependencies": { + "qrcode": "^1.5.4", + "reveal.js": "5.2.1" + }, + "devDependencies": { + "playwright": "^1.56.0" + } +} diff --git a/demo/polls.json b/demo/polls.json new file mode 100644 index 0000000..0061951 --- /dev/null +++ b/demo/polls.json @@ -0,0 +1,26 @@ +{ + "event": "htmlpptskill Live Demo", + "polls": { + "warmup": { + "type": "choice", + "kicker": "手機即時投票(示範模式)", + "title": "你平常用什麼做簡報?", + "stem": "正式使用時,觀眾掃 QR 用手機作答;這裡模擬 60 人作答。作答數穩定後按 A 揭曉,Shift+R 重來。", + "options": ["PowerPoint/Keynote", "Google 簡報", "HTML(reveal.js 等)", "其他"], + "answer": 2, + "explain": "這題沒有標準答案,示範揭曉效果:正式場合時,答案會同步推到每一支手機。" + }, + "size": { + "type": "number", + "kicker": "猜數字(示範模式)", + "title": "這份 Demo 整份 HTML 有多大?", + "stem": "reveal.js、講者備註外掛、QR、測驗、投票程式全部內嵌在同一個檔案裡。猜猜看幾 KB?", + "min": 0, + "max": 5000, + "unit": "KB", + "answer": 0, + "bins": [[0, 99], [100, 299], [300, 499], [500, 999], [1000, 1999], [2000, 5000]], + "explain": "單一檔案、拔掉網路照樣能播。數字由 build 時實際量測。" + } + } +} diff --git a/demo/src/index.html b/demo/src/index.html new file mode 100644 index 0000000..65ca41d --- /dev/null +++ b/demo/src/index.html @@ -0,0 +1,333 @@ + + + + + + + htmlpptskill Live Demo + + + + + + + + + + + +

+
+ + +
+
LIVE DEMO · 這份簡報就是用 htmlpptskill 做的
+

htmlpptskill

+
一句話讓 AI 做出 HTML 簡報
可離線播放、QR 自動驗證、觀眾手機即時投票
+
電腦按 → 翻頁 · 手機往下捲動
+ +
+ + +
+
先試試看
+

你可以這樣操作

+
+
→下一步
有些頁面會一項一項出現
+
S講者視窗
備註、下一頁、計時器
+
O總覽
所有投影片縮圖
+
F全螢幕
上台時用
+
B黑屏
請觀眾看你
+
G跳頁
輸入頁碼
+
A揭曉答案
投票頁專用
+
?全部快捷鍵
reveal.js 內建說明
+
+
這一頁示範的是 fragment:按一下出一項,聽眾不會先把整頁讀完。
+ +
+ + +
+
流程
+

從一句話到交付

+
+
1
說一句話
「幫我做一份 20 頁的講座簡報」
+
→
+
2
先對大綱
AI 列出頁碼、標題、要放的圖,等你確認
+
→
+
3
產生 HTML
reveal.js 單檔,圖片、QR 全內嵌
+
→
+
4
逐頁 QA
截圖、溢出、JS 錯誤、QR 解碼
+
→
+
5
交付
HTML + 講義 PPTX/PDF
+
+ +
+ + +
+
Auto-Animate
+

QA 到底檢查什麼?

+
逐頁 QA
+ +
+
+
Auto-Animate
+

QA 到底檢查什麼?

+
逐頁 QA:有問題就 exit 1
+
+
✓每一頁截圖,拼成一張總覽圖
+
✓文字有沒有超出版面
+
✓有沒有 JavaScript 錯誤
+
✓每一頁有沒有講者備註
+
✓每張 QR 解碼,和網址比對
+
✓離線模式:任何對外請求都算失敗
+
+
+ +
+ + +
+
3D 翻卡
+

講者會遇到的事 → 這個 skill 的做法

+
+
QR 印錯
台下掃到 404
+
逐張解碼
QA 把每張 QR 截圖解碼,和 data-url 比對
+
會場沒網路
字型、圖、外掛全掛
+
單檔內嵌
reveal.js、圖片、QR 全塞進一個 HTML
+
文字溢出
播到一半才發現
+
逐頁截圖
一張總覽圖看完全部頁面
+
主辦要 PPTX
你只有 HTML
+
一行輸出
講義 PPTX(含備註)與 PDF
+
+
按 → 四張一起翻面;點單張可以翻回來。
+ +
+ + +
+
頁內測驗
+

點點看:這是真的能作答的測驗

+
+ +
+ + +
+
+ + +
+
滑桿對比
+

同一份 AI 簡報,有沒有 QA

+
+
沒有 QA +
  • 第 7 頁標題被切掉一半
  • 結尾 QR 指向舊網址
  • 會場 Wi-Fi 一斷,字型和圖全掉
  • 第 12 頁的動畫卡住
  • 主辦單位:「可以給 PPT 嗎?」
+
有 QA +
  • ✓ 溢出:0 頁
  • ✓ QR:3 / 3 解碼相符
  • ✓ 離線:對外請求 0 個
  • ✓ JS 錯誤:0
  • ✓ 講義:deck.pptx、deck.pdf
+
+
+ +
← 拉動滑桿 →
+ +
+ + +
+
開始用
+

30 秒安裝

+
+
+
# 下載並安裝到你用的 AI coding agent +git clone https://github.com/keanu77/htmlpptskill +cd htmlpptskill && ./install.sh + +# 重開 CLI 後直接說 +「做一份 HTML 簡報」
+
支援 Claude Code、Codex CLI、Gemini CLI、Grok Build。需要 Node 18+;QA 用 Playwright,講義用 Python。
+
+
__QR__
github.com/keanu77/htmlpptskill
+
+ +
+ + +
+
MIT 授權 · 歡迎給顆星
+

謝謝觀看

+ +
這份 Demo 的原始檔在 repo 的 demo/src/index.html
+ +
+ +
+
+ + + + + + diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/TECHNIQUES.md b/docs/TECHNIQUES.md index 0bb4612..813c5da 100644 --- a/docs/TECHNIQUES.md +++ b/docs/TECHNIQUES.md @@ -350,6 +350,7 @@ node $SKILL/scripts/poll-slides.mjs index.html index-live.html --worker my-poll ``` - 產出的 `index-live.html` 內含講者控制用的 ADMIN_KEY,只放講者電腦。 +- 公開展示用模擬模式:`--polls polls.json --sim 60`,不連後端、不含 key([Live Demo](https://keanu77.github.io/htmlpptskill/) 第 8、9 頁)。 - 斷網照樣能播、能揭曉;自動化瀏覽器與 `?print-pdf` 不連線,QA 不受影響。 - 部署、題目格式、API、壓測:[`live-poll/README.md`](../skills/html-slide-deck/references/live-poll/README.md)。 diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..bf08222 Binary files /dev/null and b/docs/demo.gif differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..c160334 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,1019 @@ + + + + + + + htmlpptskill Live Demo + + + + + + + + + + + +
+
+ + +
+
LIVE DEMO · 這份簡報就是用 htmlpptskill 做的
+

htmlpptskill

+
一句話讓 AI 做出 HTML 簡報
可離線播放、QR 自動驗證、觀眾手機即時投票
+
電腦按 → 翻頁 · 手機往下捲動
+ +
+ + +
+
先試試看
+

你可以這樣操作

+
+
→下一步
有些頁面會一項一項出現
+
S講者視窗
備註、下一頁、計時器
+
O總覽
所有投影片縮圖
+
F全螢幕
上台時用
+
B黑屏
請觀眾看你
+
G跳頁
輸入頁碼
+
A揭曉答案
投票頁專用
+
?全部快捷鍵
reveal.js 內建說明
+
+
這一頁示範的是 fragment:按一下出一項,聽眾不會先把整頁讀完。
+ +
+ + +
+
流程
+

從一句話到交付

+
+
1
說一句話
「幫我做一份 20 頁的講座簡報」
+
→
+
2
先對大綱
AI 列出頁碼、標題、要放的圖,等你確認
+
→
+
3
產生 HTML
reveal.js 單檔,圖片、QR 全內嵌
+
→
+
4
逐頁 QA
截圖、溢出、JS 錯誤、QR 解碼
+
→
+
5
交付
HTML + 講義 PPTX/PDF
+
+ +
+ + +
+
Auto-Animate
+

QA 到底檢查什麼?

+
逐頁 QA
+ +
+
+
Auto-Animate
+

QA 到底檢查什麼?

+
逐頁 QA:有問題就 exit 1
+
+
✓每一頁截圖,拼成一張總覽圖
+
✓文字有沒有超出版面
+
✓有沒有 JavaScript 錯誤
+
✓每一頁有沒有講者備註
+
✓每張 QR 解碼,和網址比對
+
✓離線模式:任何對外請求都算失敗
+
+
+ +
+ + +
+
3D 翻卡
+

講者會遇到的事 → 這個 skill 的做法

+
+
QR 印錯
台下掃到 404
+
逐張解碼
QA 把每張 QR 截圖解碼,和 data-url 比對
+
會場沒網路
字型、圖、外掛全掛
+
單檔內嵌
reveal.js、圖片、QR 全塞進一個 HTML
+
文字溢出
播到一半才發現
+
逐頁截圖
一張總覽圖看完全部頁面
+
主辦要 PPTX
你只有 HTML
+
一行輸出
講義 PPTX(含備註)與 PDF
+
+
按 → 四張一起翻面;點單張可以翻回來。
+ +
+ + +
+
頁內測驗
+

點點看:這是真的能作答的測驗

+
+ +
+ + +
+
手機即時投票(示範模式)

你平常用什麼做簡報?

正式使用時,觀眾掃 QR 用手機作答;這裡模擬 60 人作答。作答數穩定後按 A 揭曉,Shift+R 重來。
A
PowerPoint/Keynote
0%
B
Google 簡報
0%
C
HTML(reveal.js 等)
0%
D
其他
0%
+
+
示範模式
模擬 60 人作答
正式使用時這裡是加入 QR
0 人作答
+
0 支手機在線
+
按 A 揭曉
+
+
+
猜數字(示範模式)

這份 Demo 整份 HTML 有多大?

reveal.js、講者備註外掛、QR、測驗、投票程式全部內嵌在同一個檔案裡。猜猜看幾 KB?
0–99
100–299
300–499
500–999
1000–1999
2000 以上
全場中位數 —
+
+
示範模式
模擬 60 人作答
正式使用時這裡是加入 QR
0 人作答
+
0 支手機在線
+
按 A 揭曉
+
+ + +
+
滑桿對比
+

同一份 AI 簡報,有沒有 QA

+
+
沒有 QA +
  • 第 7 頁標題被切掉一半
  • 結尾 QR 指向舊網址
  • 會場 Wi-Fi 一斷,字型和圖全掉
  • 第 12 頁的動畫卡住
  • 主辦單位:「可以給 PPT 嗎?」
+
有 QA +
  • ✓ 溢出:0 頁
  • ✓ QR:3 / 3 解碼相符
  • ✓ 離線:對外請求 0 個
  • ✓ JS 錯誤:0
  • ✓ 講義:deck.pptx、deck.pdf
+
+
+ +
← 拉動滑桿 →
+ +
+ + +
+
開始用
+

30 秒安裝

+
+
+
# 下載並安裝到你用的 AI coding agent +git clone https://github.com/keanu77/htmlpptskill +cd htmlpptskill && ./install.sh + +# 重開 CLI 後直接說 +「做一份 HTML 簡報」
+
支援 Claude Code、Codex CLI、Gemini CLI、Grok Build。需要 Node 18+;QA 用 Playwright,講義用 Python。
+
+
+
github.com/keanu77/htmlpptskill
+
+ +
+ + +
+
MIT 授權 · 歡迎給顆星
+

謝謝觀看

+ +
這份 Demo 的原始檔在 repo 的 demo/src/index.html
+ +
+ +
+
+ + + + + + + + + diff --git a/install.sh b/install.sh index 513bbf1..97936aa 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,7 @@ SKILLS=(html-slide-deck html-slide-effects) TARGET=all; PROJECT=0; FORCE=0 for a in "$@"; do case "$a" in --project) PROJECT=1;; --force) FORCE=1;; claude|codex|gemini|grok|all) TARGET=$a;; *) echo "未知參數 $a" >&2; exit 1;; esac; done -node -e 'process.exit(+process.versions.node.split(".")[0] >= 18 ? 0 : 1)' 2>/dev/null || echo "⚠ 需要 Node 18+(腳本用 ES modules 與 top-level await)" +node -e 'process.exit(+process.versions.node.split(".")[0] >= 20 ? 0 : 1)' 2>/dev/null || echo "⚠ 需要 Node 20+(Playwright 不支援 Node 18;Ubuntu 24.04 的 apt nodejs 是 18,請用 nvm 或 nodejs.org)" install_to() { # $1 = 目的目錄 local dest="$1"; mkdir -p "$dest" diff --git a/skills/html-slide-deck/SKILL.md b/skills/html-slide-deck/SKILL.md index 4f270e0..f8ddccb 100644 --- a/skills/html-slide-deck/SKILL.md +++ b/skills/html-slide-deck/SKILL.md @@ -6,7 +6,7 @@ description: 用 reveal.js 製作或修改 HTML 簡報(單檔、可離線、 # HTML 簡報(reveal.js)完整流程 `$SKILL` 代表本檔所在目錄(Claude Code:`~/.claude/skills/html-slide-deck`;Codex:`~/.codex/skills/…`;Gemini:`~/.gemini/skills/…`;Grok:`~/.grok/skills/…`)。腳本在 `$SKILL/scripts/`,範本在 `$SKILL/references/`。 -腳本需要 Node 18+、`playwright`(+Chromium)、`pptxgenjs`、Python 3(`segno`、`pillow`、`python-pptx`);套件從**執行目錄往上**找 `node_modules`,找不到再找 npm 全域(可設 `SKILL_PKG_ROOT`)。依賴請使用者在終端機先裝好,不要在對話中代裝。 +腳本需要 Node 20+(Playwright 不支援 18)、`playwright`(+Chromium)、`pptxgenjs`、Python 3(`segno`、`pillow`、`python-pptx`);套件從**執行目錄往上**找 `node_modules`,找不到再找 npm 全域(可設 `SKILL_PKG_ROOT`)。依賴請使用者在終端機先裝好,不要在對話中代裝。 **固定規則**:開始前先讀 `$SKILL/references/fixed-rules.md`,全程遵守(頁尾署名、課程簡報不得刪頁、醫療用語與文獻引用)。 ## 0. 先問三件事(一次問完) @@ -45,7 +45,7 @@ description: 用 reveal.js 製作或修改 HTML 簡報(單檔、可離線、 ## 3. 互動 - **課堂互動平台**(多數不能 iframe):互動頁=掃碼加入 QR+場次代碼(`.cl-code` 以 localStorage 跨頁同步,範本「範例 8」含完整頁面與腳本;`.cl-code-input` 要 `stopPropagation` 否則 reveal 吃掉按鍵)+「開啟主持畫面」新分頁按鈕+學生端截圖。範本以 `example.com` 占位,換成你的平台網址。 -- **觀眾手機即時投票**(現場有穩定網路時):`references/live-poll/`。Cloudflare Worker+Durable Object 收票、WebSocket 推到投影片;選擇題顯示百分比、猜數字顯示直方圖與中位數;講者 `A` 揭曉(手機同步顯示答案)、`Shift+R` 清空本題;斷網照樣能揭曉。HTML 寫占位 `
`、`
`,再跑 `node $SKILL/scripts/poll-slides.mjs in.html out-live.html --worker --url `。部署、建帳號前先問;**講者版 HTML 內含 ADMIN_KEY,不可外流**。完整步驟與壓測見 `references/live-poll/README.md`。 +- **觀眾手機即時投票**(現場有穩定網路時):`references/live-poll/`。Cloudflare Worker+Durable Object 收票、WebSocket 推到投影片;選擇題顯示百分比、猜數字顯示直方圖與中位數;講者 `A` 揭曉(手機同步顯示答案)、`Shift+R` 清空本題;斷網照樣能揭曉。HTML 寫占位 `
`、`
`,再跑 `node $SKILL/scripts/poll-slides.mjs in.html out-live.html --worker --url `。部署、建帳號前先問;**講者版 HTML 內含 ADMIN_KEY,不可外流**;要公開展示改用 `--polls --sim 60` 模擬模式(不連後端、不含 key)。完整步驟與壓測見 `references/live-poll/README.md`。 - **互動地圖**(場館、據點、距離):座標存 JSON,SVG 以經緯度線性投影畫點,點擊顯示名稱與到最近據點的距離、按鈕切換範圍圈;做法見 `references/interactive-pages.md`。 - **製作技巧頁**(結尾前分享怎麼做的):格式比較表+AI 協作清單,內容範例見 `references/interactive-pages.md`。 - **滑桿對比**(`viz`):適合「沒有版本控制 vs 有」這類前後對比。 diff --git a/skills/html-slide-deck/package.json b/skills/html-slide-deck/package.json index e91b97c..35aa73c 100644 --- a/skills/html-slide-deck/package.json +++ b/skills/html-slide-deck/package.json @@ -7,6 +7,6 @@ "pngjs": "^7.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" } } diff --git a/skills/html-slide-deck/references/live-poll/README.md b/skills/html-slide-deck/references/live-poll/README.md index 704561f..5d519aa 100644 --- a/skills/html-slide-deck/references/live-poll/README.md +++ b/skills/html-slide-deck/references/live-poll/README.md @@ -25,7 +25,7 @@ live-poll/ ## 1. 部署(一次) -需要 Cloudflare 帳號(免費方案即可)與 Node 18+。部署前先問使用者。 +需要 Cloudflare 帳號(免費方案即可)與 Node 20+。部署前先問使用者。 ```bash cp -R $SKILL/references/live-poll/worker my-poll && cd my-poll @@ -73,6 +73,13 @@ node $SKILL/scripts/poll-slides.mjs index.html index-live.html --worker my-poll - 做法 B(content.js):讓 build.js 對某個 type 輸出同樣的占位 section,build 完再跑這支。 - 從 PPTX 轉出的 HTML 也一樣:在轉檔程式插頁時插入占位 section。插頁後若投影片上印有頁碼,要重編。 +### 模擬模式(公開 Demo、彩排用) + +```bash +node $SKILL/scripts/poll-slides.mjs index.html demo.html --polls my-poll/src/polls.json --sim 60 +``` +不連後端、不需要 key,產出檔可以公開。進到投票頁會在幾秒內跑出 60 人的作答(選擇題約一半選正解,猜數字散在答案附近),`A` 揭曉、`Shift+R` 重跑;右側 QR 換成「示範模式」說明框。repo 的 Live Demo 就是這樣做的。 + ## 4. 現場操作 | 動作 | 做法 | diff --git a/skills/html-slide-deck/references/live-poll/screen/live-poll.css b/skills/html-slide-deck/references/live-poll/screen/live-poll.css index 471a407..9d33fe4 100644 --- a/skills/html-slide-deck/references/live-poll/screen/live-poll.css +++ b/skills/html-slide-deck/references/live-poll/screen/live-poll.css @@ -19,6 +19,8 @@ .lp-revealed .lp-opt:not(.ok){opacity:.5} .lp-side{display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center} .lp-side .qrcard{width:220px} +.lp-sim{width:220px;padding:22px 12px;border-radius:16px;border:2px dashed var(--line);color:var(--muted);font-size:18px;line-height:1.6} +.lp-sim b{color:var(--amber);font-size:22px} .lp-count{font-size:22px;margin-top:8px} .lp-count b{display:block;font-size:68px;color:var(--amber);line-height:1;font-variant-numeric:tabular-nums} .lp-phones-line{font-size:18px;color:var(--muted)} diff --git a/skills/html-slide-deck/references/live-poll/screen/live-poll.js b/skills/html-slide-deck/references/live-poll/screen/live-poll.js index 5a9ed1d..398aa73 100644 --- a/skills/html-slide-deck/references/live-poll/screen/live-poll.js +++ b/skills/html-slide-deck/references/live-poll/screen/live-poll.js @@ -1,6 +1,7 @@ /* 手機即時投票:投影片端(講者電腦)。需要 window.POLL_CFG = { url, key, polls },由 scripts/poll-slides.mjs 注入。 進到投票頁自動開題;離開投票頁手機回到「等下一題」。 講者鍵:A 揭曉、Shift+R 清空本題(彩排後用)。網路斷了照樣能按 A 顯示答案。 + 模擬模式(POLL_CFG.sim,公開 Demo 用):不連後端、不需要 key,進投票頁自動跑出 sim 人的作答過程。 網址加 ?offline 完全不連線;自動化瀏覽器(QA 截圖)與 ?print-pdf 也不連線,跟 STATIC 開關同一套判斷,要測連線加 ?live。 */ (() => { const CFG = window.POLL_CFG; @@ -12,6 +13,50 @@ const results = {}; let current = null, ws = null, wsRetry = 0, poller = null; + /* ---------------------------------------------------------------- 模擬模式 */ + const SIM = CFG.sim ? (CFG.sim === true ? 60 : CFG.sim) : 0; + const sims = {}; // id → { votes:[], timer } + function simResults(id, phase) { + const def = CFG.polls[id], v = sims[id].votes; + const out = { type: 'results', poll: id, phase, total: v.length }; + if (def.type === 'choice') { + out.counts = def.options.map((_, i) => v.filter(x => x === i).length); + } 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; + out.close = v.filter(x => Math.abs(x - def.answer) <= def.answer * 0.2).length; + } + return out; + } + function simVote(def) { + if (def.type === 'choice') { // 約一半選正解,其餘平均分散 + if (Math.random() < 0.48) return def.answer; + const others = def.options.map((_, i) => i).filter(i => i !== def.answer); + return others[Math.floor(Math.random() * others.length)]; + } + const g = Math.round(def.answer * Math.exp((Math.random() + Math.random() - 1) * 0.9)); + return Math.min(def.max, Math.max(def.min, g)); + } + function simStart(id) { + 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)); + results[id] = simResults(id, results[id] && results[id].phase === 'revealed' ? 'revealed' : 'open'); + render(id); + if (s.votes.length >= SIM) clearInterval(s.timer); + }, 220); + } + function simReset(id) { + if (sims[id]) clearInterval(sims[id].timer); + delete sims[id]; delete results[id]; + clearAnswer(id); simStart(id); + } + /* ---------------------------------------------------------------- 網路 */ async function api(path, opts = {}) { if (OFFLINE) throw new Error('offline'); @@ -110,6 +155,7 @@ async function enterPoll(id) { current = id; + if (SIM) return simStart(id); try { const res = await api('/api/results?poll=' + id); results[id] = res; @@ -128,11 +174,13 @@ async function reveal(id) { if (results[id]) results[id].phase = 'revealed'; showAnswer(id); + if (SIM) return; try { const r = await control({ poll: id, phase: 'revealed' }); results[id] = r.results; render(id); } catch (e) { setNet(false, '揭曉沒送到手機(網路)'); } } async function resetPoll(id) { + if (SIM) return simReset(id); clearAnswer(id); try { const r = await control({ poll: id, phase: 'open', reset: true }); results[id] = r.results; render(id); } catch (e) { setNet(false, '清空失敗(網路)'); } @@ -141,7 +189,7 @@ function onShow(sec) { const id = sec && sec.dataset.poll; if (id) return enterPoll(id); - if (current) { current = null; control({ poll: null }).catch(() => {}); } + if (current) { current = null; if (!SIM) control({ poll: null }).catch(() => {}); } } // capture 階段攔截,避免和 reveal.js 內建鍵衝突;只在投票頁生效 @@ -152,7 +200,8 @@ }, true); function start() { - if ($$('section[data-poll], .lp-joinslide').length) connect(); + if (SIM) setNet(true); + else if ($$('section[data-poll], .lp-joinslide').length) connect(); Reveal.on('slidechanged', e => onShow(e.currentSlide)); onShow(Reveal.getCurrentSlide()); } diff --git a/skills/html-slide-deck/references/offline-build-template/package.json b/skills/html-slide-deck/references/offline-build-template/package.json index 4fa82d3..d735cdf 100644 --- a/skills/html-slide-deck/references/offline-build-template/package.json +++ b/skills/html-slide-deck/references/offline-build-template/package.json @@ -3,7 +3,7 @@ "private": true, "version": "1.0.0", "description": "離線單檔 reveal.js 簡報(content.js+build.js)", - "engines": { "node": ">=18" }, + "engines": { "node": ">=20" }, "scripts": { "build": "node build.js", "check": "node scripts/check.mjs", diff --git a/skills/html-slide-deck/scripts/build-handout.mjs b/skills/html-slide-deck/scripts/build-handout.mjs index fb2a729..8171315 100644 --- a/skills/html-slide-deck/scripts/build-handout.mjs +++ b/skills/html-slide-deck/scripts/build-handout.mjs @@ -5,6 +5,7 @@ const pptxgen = requirePkg('pptxgenjs'); import { readFileSync, existsSync } from 'node:fs'; const [dir, out, author = ''] = process.argv.slice(2); if (!dir || !out) { console.error('usage: build-handout.mjs [author]'); process.exit(1); } +if (!existsSync(`${dir}/meta.json`)) { console.error(`找不到 ${dir}/meta.json:先跑 render-slides.mjs ${dir},並確認它成功。`); process.exit(1); } const raw = JSON.parse(readFileSync(`${dir}/meta.json`, 'utf8')); const meta = Array.isArray(raw) ? { slides: raw, bg: 'rgb(11,31,42)', width: 1280, height: 720 } : raw; // 舊版 meta.json 相容 const hex = (meta.bg.match(/\d+/g) || [11, 31, 42]).slice(0, 3).map(n => (+n).toString(16).padStart(2, '0')).join('').toUpperCase(); diff --git a/skills/html-slide-deck/scripts/poll-slides.mjs b/skills/html-slide-deck/scripts/poll-slides.mjs index 0dd9526..bc6985e 100644 --- a/skills/html-slide-deck/scripts/poll-slides.mjs +++ b/skills/html-slide-deck/scripts/poll-slides.mjs @@ -1,4 +1,5 @@ // 用法:node poll-slides.mjs --worker --url +// node poll-slides.mjs --polls --sim [人數] 模擬模式:不連後端、不含 key,可公開(Demo 用) // 把 HTML 裡的占位 section 換成手機即時投票頁,並在最後一個 前注入樣式、設定與投影片端程式。 //
→ 加入頁(大 QR+在線手機數) //
→ 投票頁(題目取自 /src/polls.json) @@ -12,10 +13,17 @@ const HERE = dirname(fileURLToPath(import.meta.url)); const SCREEN = join(HERE, '..', 'references', 'live-poll', 'screen'); const args = process.argv.slice(2); const opt = k => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : null; }; -const [input, output] = args.filter((a, i) => !a.startsWith('--') && !['--worker', '--url'].includes(args[i - 1])); +const VALUED = ['--worker', '--url', '--polls']; +const simAt = args.indexOf('--sim'); +const simN = simAt >= 0 && /^\d+$/.test(args[simAt + 1] || ''); +const SIM = simAt < 0 ? 0 : (simN ? Number(args[simAt + 1]) : 60); +const [input, output] = args.filter((a, i) => !a.startsWith('--') && !VALUED.includes(args[i - 1]) && !(simN && i === simAt + 1)); const worker = opt('--worker'), url = (opt('--url') || '').replace(/\/$/, ''); -if (!input || !output || !worker || !/^(https:\/\/|http:\/\/(localhost|127\.0\.0\.1)[:/])/.test(url + '/')) { - console.error('usage: poll-slides.mjs --worker --url (本機測試可用 http://localhost:8787)'); process.exit(1); +const pollsFile = opt('--polls') || (worker && join(worker, 'src', 'polls.json')); +const liveOk = worker && /^(https:\/\/|http:\/\/(localhost|127\.0\.0\.1)[:/])/.test(url + '/'); +if (!input || !output || !pollsFile || (!SIM && !liveOk)) { + console.error('usage: poll-slides.mjs --worker --url (本機測試可用 http://localhost:8787)\n' + + ' poll-slides.mjs --polls --sim [人數]'); process.exit(1); } function adminKey() { @@ -26,13 +34,15 @@ function adminKey() { return line.slice('ADMIN_KEY='.length).trim(); } -const { event, polls } = JSON.parse(readFileSync(join(worker, 'src', 'polls.json'), 'utf8')); +const { event, polls } = JSON.parse(readFileSync(pollsFile, 'utf8')); const QR = requirePkg('qrcode'); const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const host = url.replace(/^https?:\/\//, ''); const L = 'ABCDEFG'; +const simCard = () => `
示範模式
模擬 ${SIM} 人作答
正式使用時這裡是加入 QR
`; const qrCard = async () => { + if (SIM) return simCard(); const svg = await QR.toString(url, { type: 'svg', margin: 1, errorCorrectionLevel: 'M', color: { dark: '#0B1F2A', light: '#FFFFFF' } }); return `
${svg.replace('${esc(host)}
`; }; @@ -77,11 +87,11 @@ for (const m of [...s.matchAll(/
<\/section>|
'); process.exit(1); } -const cfg = JSON.stringify({ url, key: adminKey(), polls, event }).replace(/${readFileSync(join(SCREEN, 'live-poll.css'), 'utf8')}\n` + `\n\n`; const at = s.lastIndexOf(''); // notes.js 原始碼裡也有 ,一定要找最後一個 if (at < 0) { console.error('找不到 '); process.exit(1); } writeFileSync(output, s.slice(0, at) + inject + s.slice(at)); console.log('wrote', output, '投票頁', used.length, '題:', used.join(', ')); -console.log('⚠ 產出檔內含 ADMIN_KEY:只放講者電腦,不要上傳或寄出。'); +console.log(SIM ? `模擬模式:${SIM} 人,不含 key,可公開。` : '⚠ 產出檔內含 ADMIN_KEY:只放講者電腦,不要上傳或寄出。');