docs: add workflow lifecycle diagram and animated demo GIF to README
- Reframe the 174 skills as a DISCOVER→DECIDE→BUILD→SHIP→MEASURE→COMMUNICATE workflow with an ASCII diagram and a phase→skills table (hook for new visitors) - Replace the static playground screenshot with an animated demo GIF - Add record-demo.mjs (Playwright) to auto-generate the demo, and document it Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,40 @@
|
||||
|
||||
Images used in the main README.
|
||||
|
||||
- `playground.png` — current Skill Playground screenshot (shown in the README hero).
|
||||
- `playground-demo.gif` — animated hero demo (shown in the README).
|
||||
- `playground.png` — static screenshot / fallback.
|
||||
|
||||
## Recording the hero demo GIF
|
||||
## Re-recording the hero demo GIF (automated)
|
||||
|
||||
`record-demo.mjs` drives the live Playground with Playwright and records a video.
|
||||
The navigation, skill selection, and form-fill are real; the streamed model output
|
||||
is a representative mock, so **no API key is needed**.
|
||||
|
||||
```bash
|
||||
# 1. Serve the playground locally
|
||||
cd web && python3 -m http.server 8080 &
|
||||
|
||||
# 2. Build the skills data if it is stale
|
||||
node web/build-skills.mjs
|
||||
|
||||
# 3. Record (Playwright + a matching Chromium must be installed)
|
||||
# npx playwright install chromium # one-time, if needed
|
||||
node web/docs-assets/record-demo.mjs # writes a .webm into this folder
|
||||
|
||||
# 4. Convert the .webm to an optimized GIF (two-pass palette)
|
||||
cd web/docs-assets
|
||||
V=$(ls *.webm | head -1)
|
||||
ffmpeg -y -i "$V" -vf "fps=13,scale=1080:-1:flags=lanczos,palettegen=stats_mode=diff" /tmp/palette.png
|
||||
ffmpeg -y -i "$V" -i /tmp/palette.png \
|
||||
-lavfi "fps=13,scale=1080:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3:diff_mode=rectangle" \
|
||||
playground-demo.gif
|
||||
rm -f *.webm
|
||||
```
|
||||
|
||||
For a **fully live** recording (real Claude call instead of the mock), comment out the
|
||||
`window.fetch` override in `record-demo.mjs` and set a key via `localStorage` first.
|
||||
|
||||
## Recording manually (alternative)
|
||||
|
||||
To replace the static screenshot with a short looping demo:
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
@@ -0,0 +1,124 @@
|
||||
// Records the Skill Playground hero demo as a video, driving the real UI with
|
||||
// Playwright. The navigation, skill selection, and form-fill are genuine; only
|
||||
// the streamed model output is mocked (a representative Executive Update) so the
|
||||
// recording needs no API key. Re-run with a live key to capture a real call.
|
||||
//
|
||||
// Usage: node web/docs-assets/record-demo.mjs
|
||||
// Then convert the .webm to playground-demo.gif (see record-demo.sh).
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
// Playwright may be installed locally or only in the npx cache; resolve either.
|
||||
const require = createRequire(import.meta.url);
|
||||
const pwPath = process.env.PLAYWRIGHT_PATH || 'playwright';
|
||||
const pw = await import(pwPath).catch(() =>
|
||||
import(require.resolve('playwright', { paths: [process.env.PLAYWRIGHT_DIR].filter(Boolean) }))
|
||||
);
|
||||
const chromium = pw.chromium || (pw.default && pw.default.chromium);
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const BASE = process.env.DEMO_URL || 'http://localhost:8080/';
|
||||
const VIEWPORT = { width: 1180, height: 760 };
|
||||
|
||||
// Representative output, streamed in small chunks for a natural typing effect.
|
||||
const OUTPUT = `# Executive Update — Q2, for the CEO
|
||||
|
||||
**Headline:** Activation is up 14% after the onboarding redesign; we are on track for the Q2 target with one staffing risk to flag.
|
||||
|
||||
## Key metrics
|
||||
- **Weekly active accounts:** 12,400 (+18% QoQ)
|
||||
- **Activation rate:** 61% (+14 pts) — best quarter on record
|
||||
- **Net revenue retention:** 112% (flat)
|
||||
|
||||
## Progress
|
||||
We shipped the redesigned onboarding flow to 100% of new accounts. Early cohorts show faster time-to-value (median 2.1 days, down from 4.6).
|
||||
|
||||
## Risks & decisions needed
|
||||
- **Risk:** Backend hiring is two roles behind plan, putting the billing revamp at risk for Q3.
|
||||
- **Decision:** Approve contractor budget to hold the Q3 date.
|
||||
|
||||
## Next steps
|
||||
- Roll the activation experiment into the core product (next sprint).
|
||||
- Bring a Q3 staffing plan to the next leadership review.`;
|
||||
|
||||
function chunk(text) {
|
||||
// Split into word-ish pieces so the stream renders progressively.
|
||||
return text.match(/\S+\s*/g) || [text];
|
||||
}
|
||||
|
||||
const initScript = ({ chunks }) => {
|
||||
try { localStorage.setItem('anthropic_api_key', 'sk-ant-demo-key-not-real'); } catch (e) {}
|
||||
const realFetch = window.fetch.bind(window);
|
||||
window.fetch = (url, opts) => {
|
||||
const u = typeof url === 'string' ? url : (url && url.url) || '';
|
||||
if (!u.includes('api.anthropic.com')) return realFetch(url, opts);
|
||||
const enc = new TextEncoder();
|
||||
let i = 0;
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const push = () => {
|
||||
if (i >= chunks.length) {
|
||||
controller.enqueue(enc.encode('data: {"type":"message_stop"}\n\n'));
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
const evt = { type: 'content_block_delta', delta: { type: 'text_delta', text: chunks[i++] } };
|
||||
controller.enqueue(enc.encode('data: ' + JSON.stringify(evt) + '\n\n'));
|
||||
setTimeout(push, 45);
|
||||
};
|
||||
setTimeout(push, 200);
|
||||
},
|
||||
});
|
||||
return Promise.resolve(new Response(stream, { status: 200, headers: { 'content-type': 'text/event-stream' } }));
|
||||
};
|
||||
};
|
||||
|
||||
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const run = async () => {
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext({
|
||||
viewport: VIEWPORT,
|
||||
deviceScaleFactor: 2,
|
||||
recordVideo: { dir: __dirname, size: VIEWPORT },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.addInitScript(initScript, { chunks: chunk(OUTPUT) });
|
||||
|
||||
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||
await page.waitForSelector('.skill-card');
|
||||
await pause(700);
|
||||
|
||||
// Search to surface the featured skill.
|
||||
await page.fill('#search', 'executive update');
|
||||
await pause(600);
|
||||
|
||||
// Open it.
|
||||
await page.click('.skill-card:has(.card-title:text-is("Executive Update"))');
|
||||
await page.waitForSelector('#inputForm input, #inputForm textarea');
|
||||
await pause(500);
|
||||
|
||||
// Fill the form like a real user would.
|
||||
await page.locator('#f_0').type(
|
||||
'Shipped onboarding redesign to all new accounts. Activation up sharply. Backend hiring behind plan, billing revamp at risk for Q3.',
|
||||
{ delay: 8 }
|
||||
);
|
||||
await pause(150);
|
||||
await page.locator('#f_1').type('CEO', { delay: 25 });
|
||||
await page.locator('#f_2').type('Q2', { delay: 25 });
|
||||
await page.locator('#f_3').type('WAA, activation rate, NRR', { delay: 18 });
|
||||
await pause(500);
|
||||
|
||||
// Run — the intercepted stream renders progressively.
|
||||
await page.click('#runBtn');
|
||||
await page.waitForFunction(() => document.querySelector('#status')?.textContent?.includes('Done'), { timeout: 15000 });
|
||||
await pause(1600); // hold on the finished result
|
||||
|
||||
await context.close(); // flushes the video file
|
||||
await browser.close();
|
||||
console.log('Recorded video into', __dirname);
|
||||
};
|
||||
|
||||
run().catch((e) => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user