Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 13 additions & 30 deletions layers/registry/app/components/_SkillDemo.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { SkillDemoView } from '../../server/utils/skill-demos'
import { demoAgentIcon, demoModelLabel } from '#shared/demo-recording'
import SkillDemoShot from './_SkillDemoShot.vue'

/**
* One demo (GLOSSARY "demo"): the prompt, the output an Agent made with this
Expand All @@ -15,6 +16,10 @@ const mobileShot = computed(() => demo.shots.find(shot => shot.viewport === 'mob
const recordedLabel = computed(() => `Recorded with ${demo.agent}, ${demoModelLabel(demo.model)}`)

const live = ref(false)

/** The screens each twin window hides on; the styles below repeat them. */
const PHONE_WINDOW_MEDIA = '(max-width: 47.99rem)'
const DESKTOP_WINDOW_MEDIA = '(min-width: 48rem)'
</script>

<template>
Expand Down Expand Up @@ -66,42 +71,26 @@ const live = ref(false)
<!-- Each shot is the whole page, so it sits in a window the size of one screen and scrolls. -->
<!-- One shot per device: the phone shot on phones when one exists, the desktop shot elsewhere. -->
<div v-else class="mt-4">
<div
<SkillDemoShot
v-if="desktopShot"
:shot="desktopShot"
:skip-media="mobileShot ? PHONE_WINDOW_MEDIA : undefined"
class="skill-demo__window"
:class="{ 'skill-demo__window--desktop-only': mobileShot }"
:style="{ aspectRatio: '16 / 10' }"
:tabindex="desktopShot.height > desktopShot.width * 10 / 16 ? 0 : undefined"
role="group"
aria-label="Desktop screenshot. Scroll to see the whole page."
>
<img
:src="desktopShot.src"
:width="desktopShot.width"
:height="desktopShot.height"
:alt="desktopShot.alt"
loading="lazy"
decoding="async"
class="skill-demo__shot"
>
</div>
<div
/>
<SkillDemoShot
v-if="mobileShot"
:shot="mobileShot"
:skip-media="DESKTOP_WINDOW_MEDIA"
class="skill-demo__window skill-demo__window--mobile"
:tabindex="mobileShot.height > 844 ? 0 : undefined"
role="group"
aria-label="Phone screenshot. Scroll to see the whole page."
>
<img
:src="mobileShot.src"
:width="mobileShot.width"
:height="mobileShot.height"
:alt="mobileShot.alt"
loading="lazy"
decoding="async"
class="skill-demo__shot"
>
</div>
/>
</div>

<div class="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2">
Expand Down Expand Up @@ -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.
Expand Down
80 changes: 80 additions & 0 deletions layers/registry/app/components/_SkillDemoShot.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import type { SkillDemoShot } from '../../server/utils/skill-demos'
import { useEventListener } from '@vueuse/core'

/**
* One full-page screenshot inside a window one screen tall. The demo sits
* above the fold, so the window is the Skill page's largest paint: it loads
* the shot's first screen eagerly at high priority, and the whole page only
* once someone scrolls the window. The box already has the whole page's
* height, so the window scrolls before that image arrives.
*
* `skipMedia` names the screens where a twin window shows instead. There the
* picture resolves to an empty pixel, so a hidden window downloads nothing.
*/
const { shot, skipMedia } = defineProps<{
shot: SkillDemoShot
skipMedia?: string
}>()

/** A 1x1 transparent GIF: what the hidden twin resolves to. */
const EMPTY_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

const root = useTemplateRef<HTMLElement>('root')
const first = computed(() => shot.poster ?? shot)
const wholePage = ref(false)

useEventListener(root, 'scroll', () => {
wholePage.value = true
}, { passive: true, once: true })
</script>

<template>
<div ref="root">
<div class="skill-demo-shot" :style="{ aspectRatio: `${shot.width} / ${shot.height}` }">
<picture>
<source v-if="skipMedia" :media="skipMedia" :srcset="EMPTY_IMAGE">
<img
:src="first.src"
:width="first.width"
:height="first.height"
:alt="shot.alt"
fetchpriority="high"
decoding="async"
class="skill-demo-shot__first"
>
</picture>
<img
v-if="shot.poster && wholePage"
:src="shot.src"
:width="shot.width"
:height="shot.height"
alt=""
decoding="async"
class="skill-demo-shot__whole"
>
</div>
</div>
</template>

<style scoped>
.skill-demo-shot {
position: relative;
inline-size: 100%;
}

.skill-demo-shot picture,
.skill-demo-shot__first {
display: block;
inline-size: 100%;
block-size: auto;
}

/* Over the first screen, which it repeats, so the swap shows no seam. */
.skill-demo-shot__whole {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
}
</style>
74 changes: 59 additions & 15 deletions layers/registry/server/data/skill-demos.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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
}
}
]
},
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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
}
}
]
},
Expand All @@ -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
}
}
]
},
Expand Down
12 changes: 12 additions & 0 deletions layers/registry/server/utils/skill-demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
})),
}
}
Expand Down
Loading
Loading