Communitygithub.com

UI 示範影片錄製

使用 Playwright 錄製精美的 UI 示範影片。當使用者要求為網頁應用程式建立示範、導覽、螢幕錄影或教學影片時使用。產生具有可見游標、自然節奏和專業手感的 WebM 影片。

UI 示範影片錄製 是什麼?

這個技能讓 AI Agent 能夠錄製專業的 UI 示範影片,它使用 Playwright 驅動瀏覽器,並透過注入游標疊加層、模擬自然滑鼠移動、緩慢輸入以及自動添加字幕等功能,產生具有『可視化游標』、『自然節奏』與『專業故事感』的 WebM 影片。支援 Cursor、Claude Code、Codex 等工具,並遵循『發現 -> 排練 -> 錄製』的三階段流程。先探索目標頁面的互動元素(例如表單欄位類型、下拉選單選項、按鈕文字),然後不錄影地排練所有步驟以驗證選擇器,最後才進行錄製。錄製時會自動處理游標重疊(每次頁面導航後重新注入)、字幕顯示、平滑滾動與儀表板漫遊等細節,適合用於產品展示、功能教學、團隊入門簡報或利害關係人簡報等場景。

相容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/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>、自訂下拉選單還是組合框?
  • 選項清單:傾印選項的值 AND 文字。預留位置的值經常是 "0" 或 "",看起來非空。使用 Array.from(el.options).map(o => ({ value: o.value, text: o.text }))。跳過文字包含「Select」或值為 "0" 的選項。
  • 富文本:留言框是否支援 @提及、#標籤、Markdown 或表情符號?檢查預留位置文字。
  • 必填欄位:哪些欄位會阻止表單送出?檢查 required、標籤中的 *,並嘗試送出空白表單以查看驗證錯誤。
  • 動態內容:填入某些欄位後是否會出現其他欄位?
  • 按鈕文字:確切的文字,例如 "Submit"、"Submit Request" 或 "Send"。
  • 表格欄標題:對於使用表格驅動的模態框,應將每個 input[type="number"] 對應到其欄標題,而不是假設所有數字輸入框都代表相同含義。

輸出

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

/purchase-requests/new:
  - Budget Code: <select>(頁面上的第一個 select,4 個選項)
  - Desired Delivery: <input type="date">
  - Context: <textarea>(非 input)
  - BOM table: 內聯可編輯儲存格,具有 span.cursor-pointer -> input 模式
  - Submit: <button> 文字="Submit"

/purchase-requests/N (detail):
  - Comment: <input placeholder="Type a message..."> 支援 @user 和 #PR 標籤
  - Send: <button> 文字="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 = `排練失敗:"${label}" 未找到 - 選擇器:${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(`排練成功:"${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('排練失敗 - 請在錄製前修正選擇器');
  process.exit(1);
}
console.log('排練成功 - 所有選擇器已驗證');

當排練失敗時

  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(`警告:moveAndClick 已跳過 - "${label}" 不可見`);
    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(`警告:moveAndClick 在 "${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(`警告:typeSlowly 已跳過 - "${label}" 不可見`);
    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(`警告:panElements 跳過了元素 ${i}(選擇器:"${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);
}

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

使用模式:

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

指南:

  • 保持字幕文字簡短,理想情況下少於 60 個字元。
  • 使用 步驟 N - 動作 格式以保持一致性。
  • 在長時間停頓且 UI 本身能夠說明一切時,清除字幕。

腳本模板

'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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

相關技能