diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 878583c..5c4994c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -41,8 +41,20 @@ jobs: curl -fsSL https://d2lang.com/install.sh | sh -s -- d2 version - - name: Build package and docs assets - run: npm run docs:build + - name: Install ffmpeg + run: sudo apt-get install -y ffmpeg + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Build package + run: npm run build + + - name: Record demo GIF + run: node scripts/record-demo.mjs --out docs-site/public/demo.gif + + - name: Build docs assets and site + run: npm run docs:prepare-assets && npm --prefix docs-site run build - name: Setup Pages uses: actions/configure-pages@v4 diff --git a/README.md b/README.md index 8a368e3..95ac012 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Docs: `https://diascope.biolytics.ai` ## How it looks +![DiaScope walkthrough](docs/demo.gif) + Each story is a full-screen page: diagram on the left, narration panel on the right. Each step highlights the relevant nodes, pans/zooms to them, and shows the title + body text. Click a node for detail. Press `→` to advance. ## Interactive features diff --git a/docs-site/public/examples/vllm/deployment.html b/docs-site/public/examples/vllm/deployment.html index 8403b02..c92b838 100644 --- a/docs-site/public/examples/vllm/deployment.html +++ b/docs-site/public/examples/vllm/deployment.html @@ -205,13 +205,13 @@
-
- +
-
+ +
@@ -1088,7 +1088,7 @@ "provider access and facility trail": "Infrastructure-side evidence remains necessary even when the application stack is self-managed." }, "overview": { - "position": "first", + "position": "last", "title": "Compliant GPU Blueprint", "body": "A step-by-step walkthrough of what a compliant external GPU deployment looks like\nfor clinical AI: who controls what, where data crosses boundaries, and what evidence\nmust be in place before production traffic is allowed.\n\nUse the numbered steps to walk through the key decision points, or click any node\nfor detail on that component.\n" } diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..2f6f478 Binary files /dev/null and b/docs/demo.gif differ diff --git a/examples/vLLM/deployment.html b/examples/vLLM/deployment.html index 8403b02..c92b838 100644 --- a/examples/vLLM/deployment.html +++ b/examples/vLLM/deployment.html @@ -205,13 +205,13 @@
-
- +
-
+ +
@@ -1088,7 +1088,7 @@ "provider access and facility trail": "Infrastructure-side evidence remains necessary even when the application stack is self-managed." }, "overview": { - "position": "first", + "position": "last", "title": "Compliant GPU Blueprint", "body": "A step-by-step walkthrough of what a compliant external GPU deployment looks like\nfor clinical AI: who controls what, where data crosses boundaries, and what evidence\nmust be in place before production traffic is allowed.\n\nUse the numbered steps to walk through the key decision points, or click any node\nfor detail on that component.\n" } diff --git a/examples/vLLM/deployment.story.yaml b/examples/vLLM/deployment.story.yaml index 6bd27fe..08918a5 100644 --- a/examples/vLLM/deployment.story.yaml +++ b/examples/vLLM/deployment.story.yaml @@ -3,7 +3,7 @@ meta: d2_source: deployment.d2 overview: - position: first + position: last title: "Compliant GPU Blueprint" body: | A step-by-step walkthrough of what a compliant external GPU deployment looks like diff --git a/package-lock.json b/package-lock.json index e83adff..ad98ed0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "esbuild": "^0.21.0", + "playwright": "^1.44.0", "typescript": "^5.4.0", "vitest": "^1.5.0" } @@ -1435,6 +1436,53 @@ "dev": true, "license": "MIT" }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", diff --git a/package.json b/package.json index e32e438..0873e0f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "scripts": { "build": "tsc && node esbuild.mjs", "build:tsc": "tsc", - "docs:prepare-assets": "mkdir -p docs-site/public/examples/vllm && cp examples/vLLM/deployment.d2 docs-site/public/examples/vllm/deployment.d2 && cp examples/vLLM/deployment.story.yaml docs-site/public/examples/vllm/deployment.story.yaml && cp examples/vLLM/README.md docs-site/public/examples/vllm/README.md && node dist/cli/index.js build examples/vLLM/deployment.d2 examples/vLLM/deployment.story.yaml -o docs-site/public/examples/vllm/deployment.html && cp docs-site/public/examples/vllm/deployment.html examples/vLLM/deployment.html", + "demo:record": "node scripts/record-demo.mjs", + "docs:prepare-assets": "mkdir -p docs-site/public/examples/vllm && cp examples/vLLM/deployment.d2 docs-site/public/examples/vllm/deployment.d2 && cp examples/vLLM/deployment.story.yaml docs-site/public/examples/vllm/deployment.story.yaml && cp examples/vLLM/README.md docs-site/public/examples/vllm/README.md && node dist/cli/index.js build examples/vLLM/deployment.d2 examples/vLLM/deployment.story.yaml -o docs-site/public/examples/vllm/deployment.html && cp docs-site/public/examples/vllm/deployment.html examples/vLLM/deployment.html && ([ -f docs/demo.gif ] && cp docs/demo.gif docs-site/public/demo.gif || true)", "docs:dev": "npm --prefix docs-site run dev", "docs:build": "npm run build && npm run docs:prepare-assets && npm --prefix docs-site run build", "test": "vitest run", @@ -48,6 +49,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20.0.0", "esbuild": "^0.21.0", + "playwright": "^1.44.0", "typescript": "^5.4.0", "vitest": "^1.5.0" } diff --git a/scripts/record-demo.mjs b/scripts/record-demo.mjs new file mode 100644 index 0000000..a8ea770 --- /dev/null +++ b/scripts/record-demo.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node +/** + * Record a walkthrough GIF of the DiaScope viewer stepping through the vLLM example. + * + * Usage: + * node scripts/record-demo.mjs [--out ] + * + * Requires: + * - playwright (npm install) + * - npx playwright install chromium + * - ffmpeg on PATH + * + * Default output: docs/demo.gif + */ + +import { chromium } from 'playwright'; +import { execSync, spawnSync } from 'child_process'; +import { mkdirSync, rmSync, existsSync, readdirSync } from 'fs'; +import { join, resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '..'); + +// Parse --out flag +const outIdx = process.argv.indexOf('--out'); +const OUT_GIF = outIdx !== -1 + ? resolve(process.argv[outIdx + 1]) + : join(ROOT, 'docs', 'demo.gif'); + +const HTML = join(ROOT, 'examples', 'vLLM', 'deployment.html'); +const FRAMES_DIR = join(ROOT, '.playwright-mcp', 'demo-recording'); + +// Viewport — wide enough to show both diagram and panel clearly. +const W = 1200; +const H = 680; + +// Timing (ms) +const INITIAL_LOAD_MS = 1500; // viewer mount + first zoom-in +const FIRST_STEP_DWELL_MS = 1800; +const STEP_ANIM_MS = 1100; // zoom/pan animation per step +const STEP_DWELL_MS = 1400; // hold to read narration +const FINAL_DWELL_MS = 2200; + +// GIF output settings +const GIF_FPS = 15; +const GIF_WIDTH = 1200; // keep at full width, GitHub renders it fine + +function hasFfmpeg() { + return spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0; +} + +async function countSteps(page) { + return page.evaluate(() => { + const btns = [...document.querySelectorAll('#step-nav button')]; + // exclude the "All" button + return btns.filter(b => b.textContent?.trim() !== 'All').length; + }); +} + +async function main() { + if (!existsSync(HTML)) { + console.error(`\nBuilt HTML not found at:\n ${HTML}\n\nRun first:\n npm run build\n npm run docs:prepare-assets\n`); + process.exit(1); + } + + if (!hasFfmpeg()) { + console.error('\nffmpeg is required but not found on PATH.\n macOS: brew install ffmpeg\n Ubuntu: sudo apt-get install -y ffmpeg\n'); + process.exit(1); + } + + mkdirSync(FRAMES_DIR, { recursive: true }); + // Clean any previous recording artefacts + for (const f of readdirSync(FRAMES_DIR)) rmSync(join(FRAMES_DIR, f), { recursive: true }); + + mkdirSync(dirname(OUT_GIF), { recursive: true }); + + console.log('Launching browser…'); + const browser = await chromium.launch(); + const context = await browser.newContext({ + viewport: { width: W, height: H }, + recordVideo: { dir: FRAMES_DIR, size: { width: W, height: H } }, + }); + const page = await context.newPage(); + + console.log(`Opening ${HTML}`); + await page.goto(`file://${HTML}`); + + // Wait for the viewer shell to be ready + await page.waitForSelector('#btn-next', { state: 'visible' }); + console.log(`Waiting ${INITIAL_LOAD_MS}ms for initial render + zoom-in…`); + await page.waitForTimeout(INITIAL_LOAD_MS); + + const stepCount = await countSteps(page); + console.log(`Found ${stepCount} steps.`); + + // Dwell on step 1 + await page.waitForTimeout(FIRST_STEP_DWELL_MS); + + for (let i = 1; i < stepCount; i++) { + console.log(`Step ${i + 1} / ${stepCount}`); + await page.click('#btn-next'); + await page.waitForTimeout(STEP_ANIM_MS + STEP_DWELL_MS); + } + + // Final dwell + await page.waitForTimeout(FINAL_DWELL_MS); + + console.log('Closing browser (finalises video)…'); + await context.close(); + await browser.close(); + + // Locate the recorded .webm + const webm = readdirSync(FRAMES_DIR).find(f => f.endsWith('.webm')); + if (!webm) { + console.error('No .webm file found in recording dir.'); + process.exit(1); + } + const webmPath = join(FRAMES_DIR, webm); + console.log(`Video recorded: ${webmPath}`); + + // Convert to palette-optimised GIF + console.log(`Converting to GIF → ${OUT_GIF}`); + const ffmpegFilter = [ + `fps=${GIF_FPS}`, + `scale=${GIF_WIDTH}:-2:flags=lanczos`, + `split[s0][s1]`, + `[s0]palettegen=stats_mode=full[p]`, + `[s1][p]paletteuse=dither=bayer:bayer_scale=5`, + ].join(','); + + execSync( + `ffmpeg -y -i "${webmPath}" -vf "${ffmpegFilter}" -loop 0 "${OUT_GIF}"`, + { stdio: 'inherit' }, + ); + + console.log(`\n✓ Done: ${OUT_GIF}`); +} + +main().catch(e => { console.error(e); process.exit(1); });