Communitygithub.com

UI 演示影片錄製器

使用 Playwright 錄製精美 UI 演示影片。當使用者要求建立應用程式的示範、導覽、螢幕錄製或教學影片時使用。輸出 WebM 格式影片,包含可見游標、自然節奏與專業感。

UI 演示影片錄製器 是什麼?

此技能提供一個完整的框架,用於使用 Playwright 錄製專業的 UI 演示影片。它遵循嚴格的三階段流程:發現(探索目標頁面以了解其互動元素)、排練(在不安裝影片的情況下驗證所有選擇器)和錄製(使用游標疊加、字幕、人性化輸入和自然節奏產生最終影片)。該技能透過在每次導航後注入的 SVG 游標疊加來確保真實的游標移動,並使用「moveAndClick」和「typeSlowly」等函數來避免生硬的瞬間移動或文字填充。它還支援可自訂的字幕列,用於逐步解說和流暢滾動。此技能非常適合為文件、入職培訓或利害關係人簡報製作演示、導覽、螢幕錄製或教學影片,並且與 Playwright 相容。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/ui-demo

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

UI 演示影片錄製器 是做什麼的?

使用 Playwright 的影片錄製功能,搭配注入的游標疊加、自然節奏和敘事流程,錄製精美的網頁應用程式演示影片。

使用時機

  • 使用者要求製作「演示影片」、「螢幕錄製」、「導覽」或「教學影片」
  • 使用者想要以視覺方式展示某個功能或工作流程
  • 使用者需要為文件、入職培訓或利害關係人簡報準備影片

三階段流程

每個演示都需經歷三個階段:發現 -> 排練 -> 錄製。切勿直接跳過到錄製階段。


階段 1:發現

在撰寫任何腳本之前,先探索目標頁面,以了解實際存在的內容。

為什麼

你無法為沒見過的內容編寫腳本。欄位可能是 <input> 而不是 <textarea>,下拉選單可能是自訂元件而不是 <select>,留言框可能支援 @提及 或 #標籤。假設會在不發出任何錯誤的情況下破壞錄製。

如何做

導航到流程中的每個頁面,並傾印其互動元素:

// 在撰寫演示腳本之前,為流程中的每個頁面執行此操作
const fields = await page.evaluate(() => {
  const els = [];
  document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
    if (el.offsetParent !== null) {
      els.push({
        tag: el.tagName,
        type: el.type || '',
        name: el.name || '',
        placeholder: el.placeholder || '',
        text: el.textContent?.trim().substring(0, 40) || '',
        contentEditable: el.contentEditable === 'true',
        role: el.getAttribute('role') || '',
      });
    }
  });
  return els;
});
console.log(JSON.stringify(fields, null, 2));

需要觀察什麼

  • 表單欄位:它們是 <select>、<input>、自訂下拉選單還是組合框?
  • 選項值:傾印選項的值和文字。佔位符通常有 value="0" 或 value="",看起來非空。使用 Array.from(el.options).map(o => ({ value: o.value, text: o.text }))。跳過文字包含「Select」或值為 "0" 的選項。
  • 豐富文字:留言框是否支援 @提及、#標籤、Markdown 或表情符號?檢查 placeholder 文字。
  • 必填欄位:哪些欄位會阻止表單提交?檢查 required、標籤中的 *,並嘗試提交空值以查看驗證錯誤。
  • 動態內容:欄位是否在其他欄位填寫後才出現?
  • 按鈕標籤:確切文字,例如 「Submit」、「Submit Request」 或 「Send」。
  • 表格欄標題:對於表格驅動的模態框,將每個 input[type="number"] 對應到其欄標題,而不是假設所有數字輸入都代表相同含義。

輸出

每個頁面的欄位對應圖,用於在腳本中編寫正確的選擇器。範例:

/purchase-requests/new:
  - 預算代碼:<select> (頁面上的第一個 select,4 個選項)
  - 期望交貨日期:<input type="date">
  - 背景說明:<textarea> (不是 input)
  - BOM 表格:內聯可編輯儲存格,包含 span.cursor-pointer -> input 模式
  - 提交按鈕:<button> text="Submit"

/purchase-requests/N (明細):
  - 留言:<input placeholder="輸入訊息..."> 支援 @user 和 #PR 標籤
  - 傳送:<button> text="Send" (在輸入內容之前處於禁用狀態)

階段 2:排練

在不安裝錄製的情況下執行所有步驟。驗證每個選擇器都能解析。

為什麼

無聲的選擇器失敗是演示錄製中斷的主要原因。排練可以在浪費錄製之前發現它們。

如何做

使用 ensureVisible,一個包裝器,會記錄日誌並大聲失敗:

async function ensureVisible(page, locator, label) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    const msg = `REHEARSAL FAIL: "${label}" not found - selector: ${typeof locator === 'string' ? locator : '(locator object)'}`;
    console.error(msg);
    const found = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
        .filter(el => el.offsetParent !== null)
        .map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
        .join('\n  ');
    });
    console.error('  可見元素:\n  ' + found);
    return false;
  }
  console.log(`REHEARSAL OK: "${label}"`);
  return true;
}

排練腳本結構

const steps = [
  { label: '登入電子郵件欄位', selector: '#email' },
  { label: '登入提交按鈕', selector: 'button[type="submit"]' },
  { label: '新增請求按鈕', selector: 'button:has-text("New Request")' },
  { label: '預算代碼下拉選單', selector: 'select' },
  { label: '交貨日期', selector: 'input[type="date"]:visible' },
  { label: '描述欄位', selector: 'textarea:visible' },
  { label: '新增項目按鈕', selector: 'button:has-text("Add Item")' },
  { label: '提交按鈕', selector: 'button:has-text("Submit")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('REHEARSAL FAILED - 在錄製前修復選擇器');
  process.exit(1);
}
console.log('REHEARSAL PASSED - 所有選擇器已驗證');

當排練失敗時

  1. 閱讀可見元素的傾印。
  2. 找到正確的選擇器。
  3. 更新腳本。
  4. 重新執行排練。
  5. 只有在每個選擇器都通過時才繼續。

階段 3:錄製

只有在發現和排練都通過後,才應建立錄製。

錄製原則

1. 敘事流程

將影片規劃成一個故事。遵循使用者指定的順序,或使用此預設順序:

  • 進入點:登入或導航到起始點
  • 背景說明:環視周圍環境,讓觀眾定位
  • 操作:執行主要工作流程步驟
  • 變化:顯示次要功能,例如設定、主題或本地化
  • 結果:顯示結果、確認訊息或新狀態

2. 節奏

  • 登入後:4s
  • 導航後:3s
  • 點擊按鈕後:2s
  • 主要步驟之間:1.5-2s
  • 最終操作後:3s
  • 打字延遲:每個字元 25-40ms

3. 游標疊加

注入追隨滑鼠移動的 SVG 箭頭游標:

async function injectCursor(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-cursor')) return;
    const cursor = document.createElement('div');
    cursor.id = 'demo-cursor';
    cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
    cursor.style.cssText = `
      position: fixed; z-index: 999999; pointer-events: none;
      width: 24px; height: 24px;
      transition: left 0.1s, top 0.1s;
      filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
    `;
    cursor.style.left = '0px';
    cursor.style.top = '0px';
    document.body.appendChild(cursor);
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });
  });
}

在每次頁面導航後呼叫 injectCursor(page),因為疊加層會在導航時被銷毀。

4. 滑鼠移動

切勿瞬間移動游標。在點擊之前移動到目標:

async function moveAndClick(page, locator, label, opts = {}) {
  const { postClickDelay = 800, ...clickOpts } = opts;
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`WARNING: moveAndClick skipped - "${label}" not visible`);
    return false;
  }
  try {
    await el.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const box = await el.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
      await page.waitForTimeout(400);
    }
    await el.click(clickOpts);
  } catch (e) {
    console.error(`WARNING: moveAndClick failed on "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

每次呼叫都應包含描述性的 label 以便除錯。

5. 打字

可見地打字,而不是即時填充:

async function typeSlowly(page, locator, text, label, charDelay = 35) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`WARNING: typeSlowly skipped - "${label}" not visible`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. 滾動

使用平滑滾動而不是跳躍:

await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);

7. 儀表板掃視

在顯示儀表板或概覽頁面時,將游標移動到關鍵元素上:

async function panElements(page, selector, maxCount = 6) {
  const elements = await page.locator(selector).all();
  for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
    try {
      const box = await elements[i].boundingBox();
      if (box && box.y < 700) {
        await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
        await page.waitForTimeout(600);
      }
    } catch (e) {
      console.warn(`WARNING: panElements skipped element ${i} (selector: "${selector}"): ${e.message}`);
    }
  }
}

8. 字幕

在視窗底部注入字幕列:

async function injectSubtitleBar(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-subtitle')) return;
    const bar = document.createElement('div');
    bar.id = 'demo-subtitle';
    bar.style.cssText = `
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
      text-align: center; padding: 12px 24px;
      background: rgba(0, 0, 0, 0.75);
      color: white; font-family: -apple-system, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
      transition: opacity 0.3s;
      pointer-events: none;
    `;
    bar.textContent = '';
    bar.style.opacity = '0';
    document.body.appendChild(bar);
  });
}

async function showSubtitle(page, text) {
  await page.evaluate((t) => {
    const bar = document.getElementById('demo-subtitle');
    if (!bar) return;
    if (t) {
      bar.textContent = t;
      bar.style.opacity = '1';
    } else {
      bar.style.opacity = '0';
    }
  }, text);
  if (text) await page.waitForTimeout(800);
}

在每次導航後,與 injectCursor(page) 一起呼叫 injectSubtitleBar(page)。

使用模式:

await showSubtitle(page, '步驟 1 - 登入中');
await showSubtitle(page, '步驟 2 - 儀表板概覽');
await showSubtitle(page, '');

指南:

  • 保持字幕文字簡短,最好在 60 個字元以內。
  • 使用 步驟 N - 動作 格式以保持一致性。
  • 在長時間停頓且介面可以自我說明時,清除字幕。

腳本範本

'use strict';
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
const VIDEO_DIR = path.join(__dirname, 'screenshots');
const OUTPUT_NAME = 'demo-FEATURE.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// 在此貼上 injectCursor、injectSubtitleBar、showSubtitle、moveAndClick、
// typeSlowly、ensureVisible 和 panElements。

(async () => {
  const browser = await chromium.launch({ headless: true });

  if (REHEARSAL) {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    // 導航流程並為每個選擇器執行 ensureVisible。
    await browser.close();
    return;
  }

  const context = await browser.newContext({
    recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();

  try {
    await injectCursor(page);
    await injectSubtitleBar(page);

    await showSubtitle(page, '步驟 1 - 登入中');
    // 登入操作

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, '步驟 2 - 儀表板概覽');
    // 掃視儀表板

    await showSubtitle(page, '步驟 3 - 主要工作流程');
    // 操作序列

    await showSubtitle(page, '步驟 4 - 結果');
    // 最終揭示
    await showSubtitle(page, '');
  } catch (err) {
    console.error('演示錯誤:', err.message);
  } finally {
    await context.close();
    const video = page.video();
    if (video) {
      const src = await video.path();
      const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
      try {
        fs.copyFileSync(src, dest);
        console.log('影片已儲存:', dest);
      } catch (e) {
        console.error('錯誤:複製影片失敗:', e.message);
        console.error('  來源:', src);
        console.error('  目標:', dest);
      }
    }
    await browser.close();
  }
})();

使用方式:

# 階段 2:排練
node demo-script.cjs --rehearse

# 階段 3:錄製
node demo-script.cjs

錄製前檢查清單

  • 發現階段已完成
  • 排練通過且所有選擇器正常
  • 已啟用無頭模式
  • 解析度設定為 1280x720
  • 在每次導航後重新注入游標和字幕疊加層
  • 在主要轉場時使用 showSubtitle(page, '步驟 N - ...')
  • 所有點擊都使用 moveAndClick 並附帶描述性標籤
  • 可見輸入使用 typeSlowly
  • 沒有無聲的捕獲(catch)

…

[為翻譯而截斷]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相關技能