ui-demo
Record polished UI demo videos using Playwright. Use when the user asks to create a demo, walkthrough, screen recording, or tutorial video of a web application. Produces WebM videos with visible cursor, natural pacing, and professional feel.
DeepseekModel
Curated skill
Quality Excellent · 90
v1.0.0
Get
https://deepseekmodel.com/api/download.php?id=affaan-m-ecc-skills-ui-demo-skill-md&format=skill
Download .skill
Standard format with system_prompt and model_config, ready for any agent framework
The actual content of the system_prompt field in the .skill file.
name ui-demo description Record polished UI demo videos using Playwright. Use when the user asks to create a demo, walkthrough, screen recording, or tutorial video of a web application. Produces WebM videos with visible cursor, natural pacing, and professional feel. metadata {"origin":"ECC"} UI Demo Video Recorder Record polished demo videos of web applications using Playwright's video recording with an injected cursor overlay, natural pacing, and storytelling flow. When to Use User asks for a "demo video", "screen recording", "walkthrough", or "tutorial" User wants to showcase a feature or workflow visually User needs a video for documentation, onboarding, or stakeholder presentation Three-Phase Process Every demo goes through three phases: Discover -> Rehearse -> Record . Never skip straight to recording. Phase 1: Discover Before writing any script, explore the target pages to understand what is actually there. Why You cannot script what you have not seen. Fields may be <input> not <textarea> , dropdowns may be custom components not <select> , and comment boxes may support @mentions or #tags . Assumptions break recordings silently. How Navigate to each page in the flow and dump its interactive elements: // Run this for each page in the flow BEFORE writing the demo script 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 )); What to look for Form fields : Are they <select> , <input> , custom dropdowns, or comboboxes? Select options : Dump option values AND text. Placeholders often have value="0" or value="" which looks non-empty. Use Array.from(el.options).map(o => ({ value: o.value, text: o.text })) . Skip options where text includes "Select" or value is "0" . Rich text : Does the comment box support @mentions , #tags , markdown, or emoji? Check placeholder text. Required fields : Which fields block form submission? Check required , * in labels, and try submitting empty to see validation errors. Dynamic content : Do fields appear after other fields are filled? Button labels : Exact text such as "Submit" , "Submit Request" , or "Send" . Table column headers : For table-driven modals, map each input[type="number"] to its column header instead of assuming all numeric inputs mean the same thing. Output A field map for each page, used to write correct selectors in the script. Example: /purchase-requests/new: - Budget Code: <select> (first select on page, 4 options) - Desired Delivery: <input type="date"> - Context: <textarea> (not input) - BOM table: inline-editable cells with span.cursor-pointer -> input pattern - Submit: <button> text="Submit" /purchase-requests/N (detail): - Comment: <input placeholder="Type a message..."> supports @user and #PR tags - Send: <button> text="Send" (disabled until input has content) Phase 2: Rehearse Run through all steps without recording. Verify every selector resolves. Why Silent selector failures are the main reason demo recordings break. Rehearsal catches them before you waste a recording. How Use ensureVisible , a wrapper that logs and fails loudly: 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 ( ' Visible elements:\n ' + found); return false ; } console . log ( `REHEARSAL OK: " ${label} "` ); return true ; } Rehearsal script structure const steps = [ { label : 'Login email field' , selector : '#email' }, { label : 'Login submit' , selector : 'button[type="submit"]' }, { label : 'New Request button' , selector : 'button:has-text("New Request")' }, { label : 'Budget Code select' , selector : 'select' }, { label : 'Delivery date' , selector : 'input[type="date"]:visible' }, { label : 'Description field' , selector : 'textarea:visible' }, { label : 'Add Item button' , selector : 'button:has-text("Add Item")' }, { label : 'Submit button' , 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 - fix selectors before recording' ); process. exit ( 1 ); } console . log ( 'REHEARSAL PASSED - all selectors verified' ); When rehearsal fails Read the visible-element dump. Find the correct selector. Update the script. Re-run rehearsal. Only proceed when every selector passes. Phase 3: Record Only after discovery and rehearsal pass should you create the recording. Recording Principles 1. Storytelling Flow Plan the video as a story. Follow user-specified order, or use this default: Entry : Login or navigate to the starting point Context : Pan the surroundings so viewers orient themselves Action : Perform the main workflow steps Variation : Show a secondary feature such as settings, theme, or localization Result : Show the outcome, confirmation, or new state 2. Pacing After login: 4s After navigation: 3s After clicking a button: 2s Between major steps: 1.5-2s After the final action: 3s Typing delay: 25-40ms per character 3. Cursor Overlay Inject an SVG arrow cursor that follows mouse movements: 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' ; }); }); } Call injectCursor(page) after every page navigation because the overlay is destroyed on navigate. 4. Mouse Movement Never teleport the cursor. Move to the target before clicking: 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 ; } Every call should include a descriptive label for debugging. 5. Typing Type visibly, not instant-fill: 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. Scrolling Use smooth scroll instead of jumps: await page. evaluate ( () => window . scrollTo ({ top : 400 , behavior : 'smooth' })); await page. waitForTimeout ( 1500 ); 7. Dashboard Panning When showing a dashboard or overview page, move the cursor across key elements: 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. Subtitles Inject a subtitle bar at the bottom of the viewport: 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 ); } Call injectSubtitleBar(page) alongside injectCursor(page) after every navigation. Usage pattern: await showSubtitle (page, 'Step 1 - Logging in' ); await showSubtitle (page, 'Step 2 - Dashboard overview' ); await showSubtitle (page, '' );
Keywords that activate this skill. Click one to copy it.
This skill does not provide trigger words.
The downloaded .skill package contains the following fields.
| Field | Description |
|---|---|
| format | Format tag (skill/v1) |
| skill_id | Unique skill ID |
| name | Skill name |
| version | Version |
| description | Description |
| category | Categories (array) |
| trigger_words | Trigger words |
| tags | Tags |
| source | Source |
| source_url | Source URL (this page) |
| exported_at | Exported at (set per download) |
| system_prompt | System prompt body |
| model_config | Model config: provider / model / temperature / max_tokens / top_p |
| examples | Examples |
| install_guide | Import guide for Coze / Dify / Claude / custom frameworks |
The same skill can be exported in different platform formats.