Communitygithub.com

UI演示视频录制器

使用Playwright录制精美的UI演示视频。在用户要求创建演示、操作引导、屏幕录制或Web应用程序教程视频时使用。生成带有可见光标、自然节奏和专业感的WebM视频。

UI演示视频录制器 是什么?

UI Demo Video Recorder 是一个基于Playwright的技能,旨在帮助开发者轻松录制高质量的Web应用演示视频。它通过三个阶段的工作流程(探索、排练、录制)确保视频录制过程稳定、专业。录制前,技能会探索目标页面并生成字段映射,避免选择器错误;排练阶段会验证所有选择器,防止录制失败;最后录制阶段采用自然的光标移动、节奏控制、字幕注入和SVG光标覆盖,生成带有专业感的WebM视频。该技能适合用于文档、新员工入职培训或向利益相关者演示工作流程,支持自定义脚本模板,并推荐在无头模式下运行。

兼容平台~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的视频录制功能,配合注入的光标叠加、自然节奏和叙事流程,录制精美的Web应用演示视频。

使用时机

  • 用户要求制作“演示视频”、“屏幕录制”、“操作引导”或“教程”
  • 用户希望通过可视化方式展示某个功能或工作流程
  • 用户需要为文档、新员工入职培训或利益相关者演示制作视频

三阶段流程

每个演示都经过三个阶段:探索 -> 排练 -> 录制。切勿直接跳至录制阶段。


阶段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 文本。占位符常带有 value="0" 或 value="",看起来非空。使用 Array.from(el.options).map(o => ({ value: o.value, text: o.text }))。跳过文本包含“选择”或值为 "0" 的选项。
  • 富文本:评论框是否支持 @提及、#标签、Markdown 或表情符号?检查占位符文本。
  • 必填字段:哪些字段会阻止表单提交?检查 required、标签中的 *,并尝试在空状态下提交以查看验证错误。
  • 动态内容:填充其他字段后是否会出现新字段?
  • 按钮文本:确切的文本,例如 "提交"、"提交请求" 或 "发送"。
  • 表格列标题:对于表格驱动的模态框,将每个 input[type="number"] 映射到其列标题,而不是假设所有数字输入含义相同。

输出

每个页面的字段映射,用于在脚本中编写正确的选择器。示例:

/purchase-requests/new:
  - 预算代码: <select> (页面上的第一个 select,4个选项)
  - 期望送达日期: <input type="date">
  - 上下文: <textarea> (不是 input)
  - BOM 表格: 使用 span.cursor-pointer -> input 模式的内联可编辑单元格
  - 提交按钮: <button> text="提交"

/purchase-requests/N (详情):
  - 评论: <input placeholder="输入消息..."> 支持 @用户 和 #PR 标签
  - 发送按钮: <button> text="发送" (输入内容前处于禁用状态)

阶段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 对象)'}`;
    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("新建请求")' },
  { label: '预算代码选择器', selector: 'select' },
  { label: '送达日期', selector: 'input[type="date"]:visible' },
  { label: '描述字段', selector: 'textarea:visible' },
  { label: '添加项目按钮', selector: 'button:has-text("添加项目")' },
  { label: '提交按钮', selector: 'button:has-text("提交")' },
];

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(`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);
}

每次导航后,将 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
  • 无静默捕获;

…

[为翻译截断]

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.

相关技能