@@ -171,12 +160,6 @@ const live = ref(false)
outline-offset: 2px;
}
-.skill-demo__shot {
- display: block;
- width: 100%;
- height: auto;
-}
-
/*
* Full width and a fixed height, not an aspect ratio: a max-height on an
* aspect-ratio box shrinks its width too, which left a narrow strip.
diff --git a/layers/registry/app/components/_SkillDemoShot.vue b/layers/registry/app/components/_SkillDemoShot.vue
new file mode 100644
index 00000000..14b0e5ce
--- /dev/null
+++ b/layers/registry/app/components/_SkillDemoShot.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/layers/registry/server/data/skill-demos.json b/layers/registry/server/data/skill-demos.json
index ba77365b..5c3c5e14 100644
--- a/layers/registry/server/data/skill-demos.json
+++ b/layers/registry/server/data/skill-demos.json
@@ -14,18 +14,26 @@
"outputFile": "index.html",
"shots": [
{
- "file": "desktop-2a201b3766.jpg",
+ "file": "desktop-d514027082.jpg",
"width": 1440,
"height": 4102,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i"
+ "alt": "Desktop screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i",
+ "poster": {
+ "file": "desktop-poster-3180cdf6a9.jpg",
+ "height": 900
+ }
},
{
- "file": "mobile-da8e537494.jpg",
+ "file": "mobile-32b72a3ea8.jpg",
"width": 390,
"height": 5065,
"viewport": "mobile",
- "alt": "Phone screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i"
+ "alt": "Phone screenshot of the page the Agent built for: Build a single-file landing page for Tidepool, a tide-chart app for surfers. Inline all CSS and JavaScript. Save it as i",
+ "poster": {
+ "file": "mobile-poster-f9b907cc90.jpg",
+ "height": 844
+ }
}
],
"authorName": "Anthropic",
@@ -49,7 +57,11 @@
"width": 1440,
"height": 927,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Keep the default style guide. Draw an architecture diagram for Tidewhisk, a fictional ferry-booking service: web app, AP"
+ "alt": "Desktop screenshot of the page the Agent built for: Keep the default style guide. Draw an architecture diagram for Tidewhisk, a fictional ferry-booking service: web app, AP",
+ "poster": {
+ "file": "desktop-poster-caff9f689a.jpg",
+ "height": 900
+ }
}
],
"authorName": "Cathryn Lavery",
@@ -75,14 +87,22 @@
"width": 1440,
"height": 6000,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per lang"
+ "alt": "Desktop screenshot of the page the Agent built for: Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per lang",
+ "poster": {
+ "file": "desktop-poster-2aeacf6003.jpg",
+ "height": 900
+ }
},
{
"file": "mobile-4d54ba1970.jpg",
"width": 390,
"height": 6000,
"viewport": "mobile",
- "alt": "Phone screenshot of the page the Agent built for: Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per lang"
+ "alt": "Phone screenshot of the page the Agent built for: Build a landing page for Glossmere, a fictional invoicing app for freelance translators that bills per word and per lang",
+ "poster": {
+ "file": "mobile-poster-6ab5a5de6f.jpg",
+ "height": 844
+ }
}
]
},
@@ -224,11 +244,15 @@
"outputFile": "flow.html",
"shots": [
{
- "file": "desktop-70a1bca51d.jpg",
+ "file": "desktop-5bb32b6afc.jpg",
"width": 1440,
"height": 1060,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Show me how Cogwhistle, a fictional bike-courier dispatch app, assigns a new delivery to a rider, from order to pickup. "
+ "alt": "Desktop screenshot of the page the Agent built for: Show me how Cogwhistle, a fictional bike-courier dispatch app, assigns a new delivery to a rider, from order to pickup. ",
+ "poster": {
+ "file": "desktop-poster-2dbf5169f1.jpg",
+ "height": 900
+ }
}
],
"authorName": "HumanLayer",
@@ -252,14 +276,22 @@
"width": 1440,
"height": 1164,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Build pricing.html, a single-file HTML pricing section for Glintwork, a fictional photo-editing app: three plan cards, a"
+ "alt": "Desktop screenshot of the page the Agent built for: Build pricing.html, a single-file HTML pricing section for Glintwork, a fictional photo-editing app: three plan cards, a",
+ "poster": {
+ "file": "desktop-poster-21e78314b8.jpg",
+ "height": 900
+ }
},
{
"file": "mobile-7180f99b12.jpg",
"width": 390,
"height": 2192,
"viewport": "mobile",
- "alt": "Phone screenshot of the page the Agent built for: Build pricing.html, a single-file HTML pricing section for Glintwork, a fictional photo-editing app: three plan cards, a"
+ "alt": "Phone screenshot of the page the Agent built for: Build pricing.html, a single-file HTML pricing section for Glintwork, a fictional photo-editing app: three plan cards, a",
+ "poster": {
+ "file": "mobile-poster-8abcea89bd.jpg",
+ "height": 844
+ }
}
],
"authorName": "Jakub Krehel",
@@ -348,14 +380,22 @@
"width": 1440,
"height": 905,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Roa"
+ "alt": "Desktop screenshot of the page the Agent built for: Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Roa",
+ "poster": {
+ "file": "desktop-poster-8fa468cdbd.jpg",
+ "height": 900
+ }
},
{
"file": "mobile-2c917bf37f.jpg",
"width": 390,
"height": 1751,
"viewport": "mobile",
- "alt": "Phone screenshot of the page the Agent built for: Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Roa"
+ "alt": "Phone screenshot of the page the Agent built for: Build a one-page dashboard for Wrenfold Bikes, a town bike share. This week's rides by station: Harbour 412, Station Roa",
+ "poster": {
+ "file": "mobile-poster-4e7f952eb1.jpg",
+ "height": 844
+ }
}
]
},
@@ -375,11 +415,15 @@
"outputFile": "index.html",
"shots": [
{
- "file": "desktop-5912c3c3eb.jpg",
+ "file": "desktop-2bcdf1a732.jpg",
"width": 1440,
"height": 6000,
"viewport": "desktop",
- "alt": "Desktop screenshot of the page the Agent built for: Build a landing page for Kilnmarrow, a fictional shared pottery studio that rents kiln shelf space by the firing. Don't "
+ "alt": "Desktop screenshot of the page the Agent built for: Build a landing page for Kilnmarrow, a fictional shared pottery studio that rents kiln shelf space by the firing. Don't ",
+ "poster": {
+ "file": "desktop-poster-77ef6d0637.jpg",
+ "height": 900
+ }
}
]
},
diff --git a/layers/registry/server/utils/skill-demos.ts b/layers/registry/server/utils/skill-demos.ts
index 6be474b6..fd2316ae 100644
--- a/layers/registry/server/utils/skill-demos.ts
+++ b/layers/registry/server/utils/skill-demos.ts
@@ -27,6 +27,15 @@ const shotSchema = z.object({
height: z.number().int().positive(),
alt: z.string().min(1),
viewport: z.enum(['desktop', 'mobile']),
+ /**
+ * The first screen of a shot taller than one screen, at the shot's width and
+ * the viewport's height. The Skill page paints it first and loads the full
+ * page only when someone scrolls the window.
+ */
+ poster: z.object({
+ file: z.string().regex(/^[\w-]+\.jpg$/),
+ height: z.number().int().positive(),
+ }).optional(),
})
const videoSchema = z.object({
@@ -102,6 +111,8 @@ export interface SkillDemoShot {
height: number
alt: string
viewport: 'desktop' | 'mobile'
+ /** The first screen, when the shot runs longer than one. */
+ poster: { src: string, width: number, height: number } | null
}
export interface SkillDemoVideo {
@@ -170,6 +181,7 @@ export function presentSkillDemo(demo: SkillDemoRecord, currentCommit: string |
height: shot.height,
alt: shot.alt,
viewport: shot.viewport,
+ poster: shot.poster ? { src: `${media}/${shot.poster.file}`, width: shot.width, height: shot.poster.height } : null,
})),
}
}
diff --git a/scripts/record-skill-demo.ts b/scripts/record-skill-demo.ts
index affe00b3..91baf5e4 100644
--- a/scripts/record-skill-demo.ts
+++ b/scripts/record-skill-demo.ts
@@ -15,8 +15,9 @@
* `npx skilld run`, then gets the prompt word for word. `--seed` copies a
* folder in first, and `--setup` says so on the demo.
* 3. An HTML output gets full-page screenshots at desktop width, and at phone
- * width when the page fits a phone. An MP4 output is re-encoded small and
- * gets a poster frame.
+ * width when the page fits a phone. A page taller than one screen also gets
+ * a poster: its first screen alone, which the Skill page paints first. An
+ * MP4 output is re-encoded small and gets a poster frame.
* 4. Uploads the media to the `skilld-demo-media` R2 bucket with `cf`, under
* content-hashed names, copies an HTML page to the registry's
* `server/demos/`, and upserts the entry in `server/data/skill-demos.json`.
@@ -112,6 +113,8 @@ interface Shot {
height: number
alt: string
viewport: 'desktop' | 'mobile'
+ /** The first screen, for a shot taller than one. */
+ poster?: { file: string, height: number }
}
interface Video {
@@ -317,12 +320,17 @@ async function screenshot(page: string, dir: string, prompt: string): Promise height ? `${viewport}-poster.jpg` : null
+ if (poster)
+ await tab.screenshot({ path: join(dir, poster), type: 'jpeg', quality: 82, clip: { x: 0, y: 0, width, height } })
shots.push({
file,
width,
height: shotHeight,
viewport,
alt: `${viewport === 'desktop' ? 'Desktop' : 'Phone'} screenshot of the page the Agent built for: ${prompt.slice(0, 120)}`,
+ ...(poster ? { poster: { file: poster, height } } : {}),
})
await context.close()
}
@@ -456,11 +464,14 @@ async function publishMedia(owner: string, repo: string, name: string, dir: stri
/** Stages, uploads, and renames the media of one demo. */
async function publishDemoMedia(owner: string, repo: string, name: string, dir: string, media: Pick): Promise> {
- const files = [...new Set([...media.shots.map(shot => shot.file), ...(media.video ? [media.video.file, media.video.poster] : [])])]
+ const files = [...new Set([
+ ...media.shots.flatMap(shot => shot.poster ? [shot.file, shot.poster.file] : [shot.file]),
+ ...(media.video ? [media.video.file, media.video.poster] : []),
+ ])]
const names = await publishMedia(owner, repo, name, dir, files)
const to = (file: string) => names.get(file) ?? file
return {
- shots: media.shots.map(shot => ({ ...shot, file: to(shot.file) })),
+ shots: media.shots.map(shot => ({ ...shot, file: to(shot.file), ...(shot.poster ? { poster: { ...shot.poster, file: to(shot.poster.file) } } : {}) })),
...(media.video ? { video: { ...media.video, file: to(media.video.file), poster: to(media.video.poster) } } : {}),
}
}
diff --git a/test/unit/skill-demos.test.ts b/test/unit/skill-demos.test.ts
index 556c90e0..496e93ec 100644
--- a/test/unit/skill-demos.test.ts
+++ b/test/unit/skill-demos.test.ts
@@ -39,6 +39,23 @@ describe('presentSkillDemo', () => {
})
})
+describe('presentSkillDemo first-screen posters', () => {
+ it('serves a tall shot with its first screen, at the shot width', () => {
+ const view = presentSkillDemo(demo({
+ shots: [{ file: 'desktop.jpg', width: 1440, height: 4102, alt: 'Desktop screenshot', viewport: 'desktop', poster: { file: 'desktop-poster.jpg', height: 900 } }],
+ }), null, 'https://media.example')
+ expect(view.shots[0]?.poster).toEqual({
+ src: 'https://media.example/demos/anthropics/skills/frontend-design/desktop-poster.jpg',
+ width: 1440,
+ height: 900,
+ })
+ })
+
+ it('gives a one-screen shot no poster, since the shot is its own first screen', () => {
+ expect(presentSkillDemo(demo(), null).shots[0]?.poster).toBeNull()
+ })
+})
+
describe('presentSkillDemo for a video Skill', () => {
it('serves the video and its poster, and offers no live page without one', () => {
const view = presentSkillDemo(demo({