Skip to content
← Public packages

@kentcdodds/invisible-software

Invisible software: a phone-only step-through demo plus a chart of AI agent benchmark progress.

FollowForkOpen app

src/slides/05-max-forks.html

171 lines · 11.5 KB · HTML
Raw
<section class="slide uses-vc-app uses-chat" data-label="5" data-duration="21" data-steps="[[0.4,7.45],[8.3,9.4],[10.2,10.6],[11,11.4],[12.6,14.15],[15.85,16.9],[17.45,18.2],[18.55,19.4]]" data-human="[1,5,6,7,8]">
<style>
@keyframes breathe-max-forks { 0%, 100% { transform: scale(0.7); opacity: 0.6; } 50% { transform: scale(1); opacity: 1; } }
@scope {
	.phone { left: 1150px; top: 102px; transform: scale(1.1); }
	.caption { left: 160px; width: 820px; }
	/* ChatGPT-style chat; the Blossom in the header is the official mark from openai.com/brand */
	.gpt { position: absolute; inset: 0; background: #fff; font-family: var(--ios); color: #0d0d0d; }
	.gpt-head { position: absolute; top: 54px; left: 0; right: 0; height: 52px; display: flex; align-items: center; justify-content: space-between; padding: 0 18px; }
	.gpt-head .title { font: 600 18px/1 var(--ios); display: flex; align-items: center; gap: 5px; }
	.gpt-head .title svg { color: #8f8f8f; }
	.gpt-head .ic { width: 26px; height: 26px; display: grid; place-items: center; color: #0d0d0d; }
	.gpt-empty { position: absolute; left: 0; right: 0; top: 330px; text-align: center; font: 600 24px/1.2 var(--ios); letter-spacing: -0.015em; }
	.gpt-msgs { position: absolute; left: 0; right: 0; bottom: 120px; padding: 0 18px; display: flex; flex-direction: column; }
	.gpt-me { align-self: flex-end; max-width: 300px; background: #f2f2f2; border-radius: 22px; padding: 11px 16px; font: 400 17px/1.36 var(--ios); }
	.gpt-ai { align-self: stretch; font: 400 17px/1.45 var(--ios); color: #0d0d0d; padding: 2px 2px; }
	.thinking { display: flex; align-items: center; gap: 9px; font: 500 15.5px/1 var(--ios); color: #8f8f8f; }
	.thinking .orb { width: 12px; height: 12px; border-radius: 50%; background: #0d0d0d; animation: breathe-max-forks 1.1s ease-in-out infinite; }

	.tool { align-self: flex-start; display: inline-flex; align-items: center; gap: 7px; padding: 7px 11px; border-radius: 999px; background: #f4f4f4; font: 500 13.5px/1 var(--ios); color: #5d5d5d; }
	.tool img { width: 16px; height: 16px; }
	.gpt .linkcard { width: 300px; margin-top: 12px; border: 1px solid #ececec; background: #fafafa; }
	.gpt-comp { position: absolute; left: 12px; right: 12px; bottom: 30px; min-height: 56px; border-radius: 28px; background: #fff; box-shadow: 0 2px 10px rgb(0 0 0 / 0.08), 0 0 0 1px rgb(0 0 0 / 0.08); display: flex; align-items: flex-end; gap: 10px; padding: 10px 8px 9px 14px; }
	.gpt-comp .plus { width: 34px; height: 34px; display: grid; place-items: center; color: #0d0d0d; flex: none; margin-bottom: 1px; }
	.gpt-comp .txt { flex: 1; font: 400 17px/1.36 var(--ios); padding-bottom: 6px; min-height: 23px; }
	.gpt-comp .ph { color: #9b9b9b; }
	.gpt-comp .go { width: 38px; height: 38px; border-radius: 50%; background: #0d0d0d; color: #fff; display: grid; place-items: center; flex: none; }
	.gpt-comp .go svg { fill: currentColor; }
	.vx { --vc-brass: #2f6f62; --vc-brass-soft: #dcefe9; }
	.safari { z-index: 30; }
	.safari-body { background: var(--vc-cream); }
	.scr { position: absolute; inset: 0; }
	.sched { position: absolute; inset: 0; background: #fbfaf7; font-family: var(--ios); color: #1c2230; padding: 18px 16px; }
	.sched .top { display: flex; align-items: center; gap: 10px; }
	.sched .top .av-img { width: 40px; height: 40px; }
	.sched .top b { font: 750 18px/1.1 var(--ios); display: block; }
	.sched .top small { font: 500 13px/1.2 var(--ios); color: #7b776f; }
	.sched h2 { font: 750 26px/1.1 var(--ios); letter-spacing: -0.02em; margin-top: 22px; }
	.days { display: flex; gap: 7px; margin-top: 14px; }
	.day { flex: 1; text-align: center; padding: 9px 0; border-radius: 12px; background: #fff; border: 1px solid #e7e2d8; font: 600 12px/1.25 var(--ios); color: #7b776f; }
	.day b { display: block; font-size: 18px; color: #1c2230; }
	.day.on { background: #2f6f62; border-color: #2f6f62; color: #d6ece6; }
	.day.on b { color: #fff; }
	.times { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 18px; }
	.time { padding: 13px 0; text-align: center; border-radius: 12px; border: 1.5px solid #cfe3dc; color: #2f6f62; font: 650 15px/1 var(--ios); background: #fff; }
	.time.off { border-color: #eee; color: #c3c0b9; text-decoration: line-through; }
	.time.on { background: #2f6f62; color: #fff; border-color: #2f6f62; }
	.confirm { margin-top: 18px; height: 52px; border-radius: 14px; background: #1c2230; color: #fff; display: flex; align-items: center; justify-content: center; gap: 8px; font: 650 17px/1 var(--ios); }
	.confirm.done { background: #2f6f62; }
	/* Official Square logo (squareup.com/us/en/press/logo), unmodified. */
	.powered { margin-top: 14px; display: flex; align-items: center; justify-content: center; gap: 7px; font: 500 13px/1 var(--ios); color: #7b776f; }
	.powered img { height: 17px; width: auto; display: block; }
}
</style>
<div class="stage-pattern"></div>
<div class="bug"><img src="assets/kody-mark.png">Kody<span class="dom">kody.codes</span></div>

<div class="caption">
	<div class="cap" style="animation: capIn .8s cubic-bezier(.22,1,.36,1) .4s both, capOut .5s ease 6.0s forwards">
		<div class="kicker">Max · another barber</div>
		<div class="headline">He asks for<br><em>what Victoria has</em></div>
		<div class="sub">In plain words, in the chat app he already uses.</div>
	</div>
	<div class="cap" style="animation: capIn .8s cubic-bezier(.22,1,.36,1) 6.5s both, capOut .5s ease 12.6s forwards">
		<div class="kicker">One request</div>
		<div class="headline">Forked and<br><em>made his</em></div>
		<div class="sub">ChatGPT forks Victoria's app and customizes it for him.</div>
	</div>
	<div class="cap" style="animation: capIn .8s cubic-bezier(.22,1,.36,1) 13.1s both">
		<div class="kicker">His domain</div>
		<div class="headline">Live at<br><em>his own address</em></div>
		<div class="sub">schedule.maxbarber.example<br>On <b>his own domain</b></div>
	</div>
</div>

<div class="phone"><div class="screen">
	<div class="island"></div>

	<div class="gpt">
		<div class="statusbar"></div>
		<div class="gpt-head">
			<span class="ic"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M4 8h16M4 16h10"/></svg></span>
			<span class="title"><img src="assets/chatgpt-blossom.svg" alt="" style="width:20px;height:20px">ChatGPT <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg></span>
			<span class="ic"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h8M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg></span>
		</div>
		<div class="gpt-empty" id="empty">What can I help with?</div>
		<div class="gpt-msgs">
			<div class="row me" style="--d:5.9s"><div class="gpt-me">I want what Victoria has for her barber shop, but with a Book now button that goes to my scheduler, on my own domain.</div></div>
			<div class="row gone" style="--d:6.5s;--e:10.2s"><div class="thinking"><span class="orb"></span><span id="think">Thinking</span></div></div>
			<div class="row" style="--d:10.2s"><div class="gpt-ai">Done! I forked Victoria's barber app and made it yours, with your name, photo and services. Book now goes straight to your scheduler, and it's live on your own domain.</div></div>
			<div class="row" style="--d:11.0s"><div class="linkcard vx" id="linkCard">
				<div class="lc-img">
					<div class="app-head"><img class="av-img" src="assets/max.jpg"><div><h1>Max's Barbershop</h1><p>Classic cuts · fades · beard trims</p></div></div>
					<div class="mini-btn">Book now ↗</div>
				</div>
				<div class="lc-meta"><b>Max's Barbershop · Book a cut</b><span>schedule.maxbarber.example</span></div>
			</div></div>
		</div>
		<div class="gpt-comp">
			<span class="plus" data-icon="plus"></span>
			<span class="txt"><span class="ph">Ask anything</span><span class="typed" style="display:none"></span></span>
			<span class="go" id="go"></span>
		</div>
		<div class="home-ind"></div>
	</div>

	<div class="safari" id="saf">
		<div class="statusbar"></div>
		<div class="safari-top" data-url="schedule.maxbarber.example"></div>
		<div class="safari-body">
			<div class="scr pub vx" id="pPub">
				<div class="hero"><img class="av-img" src="assets/max.jpg"><h1>Max's Barbershop</h1><p>Classic cuts · fades · beard trims<br>Open Thursday · 3 slots left</p></div>
				<div class="book-now" id="bookNow">Book now <span data-icon="arrowUR"></span></div>
				<div class="card svc">
					<div><span>Classic cut<small>45 min</small></span><b>$35</b></div>
					<div><span>Skin fade<small>50 min</small></span><b>$40</b></div>
					<div><span>Beard trim<small>20 min</small></span><b>$20</b></div>
				</div>
			</div>
			<div class="scr sched" id="pSched">
				<div class="top"><img class="av-img" src="assets/max.jpg"><div><b>Max's Barbershop</b><small>Online booking</small></div></div>
				<h2>Pick a time</h2>
				<div class="days"><div class="day">Wed<b>14</b></div><div class="day on">Thu<b>15</b></div><div class="day">Fri<b>16</b></div><div class="day">Sat<b>17</b></div><div class="day">Mon<b>19</b></div></div>
				<div class="times"><div class="time">9:30</div><div class="time off">11:00</div><div class="time" id="t2">2:00</div><div class="time">4:30</div><div class="time">5:15</div><div class="time off">6:00</div></div>
				<div class="confirm" id="confirm">Confirm 2:00</div>
				<div class="powered">Booking &amp; payments by <img src="assets/square-logo.svg" alt="Square"></div>
			</div>
		</div>
		<div class="safari-bottom"></div>
	</div>
	<div class="home-ind" style="z-index:60;opacity:0" id="safHome"></div>
</div></div>
<div class="stage-vignette"></div>
<script>
deck.slide(({ root, onTime, typer, tapOn, openFrom }) => {
	const $ = (s) => root.querySelector(s)
	const prompt = 'I want what Victoria has for her barber shop, but with a Book now button that goes to my scheduler, on my own domain.'
	const T_OPEN = 13.3
	tapOn('.gpt-comp', 1.0)
	tapOn('#go', 5.5)
	tapOn('#linkCard', 12.8)
	tapOn('#bookNow', 16.0)
	tapOn('#t2', 17.6)
	tapOn('#confirm', 18.7)
	openFrom($('#saf'), '#linkCard', T_OPEN)
	const ph = $('.gpt-comp .ph'), typed = $('.gpt-comp .typed')
	typer(typed, prompt, 1.3, 30)
	const think = ['Thinking', 'Finding Victoria\'s barber app', 'Forking it for you', 'Adding Book now and your domain']
	onTime((t) => {
		const composing = t >= 1.15 && t < 5.9
		ph.style.display = composing ? 'none' : ''
		typed.style.display = composing ? '' : 'none'
		typed.classList.toggle('caret', composing)
		$('#go').innerHTML = composing && t >= 1.35 ? icon.send : icon.wave
		$('#empty').style.opacity = 1 - prog(t, 5.6, 5.9)
		$('#think').textContent = think[Math.min(3, Math.floor(clamp((t - 6.5) / 0.95, 0, 3)))]
		$('#safHome').style.opacity = t >= T_OPEN + 0.6 ? 1 : 0
		const sched = t >= 16.3
		$('#pPub').style.opacity = 1 - prog(t, 16.3, 16.65)
		$('#pSched').style.opacity = prog(t, 16.45, 16.85)
		const pr = $('.url-progress')
		const lp = t >= 16.3 ? prog(t, 16.3, 16.9) : prog(t, T_OPEN, T_OPEN + 0.8)
		pr.style.width = lp * 100 + '%'; pr.style.opacity = lp > 0 && lp < 1 ? 1 : 0
		$('#t2').classList.toggle('on', t >= 17.75)
		const c = $('#confirm')
		c.classList.toggle('done', t >= 19.3)
		c.innerHTML = t < 18.85 ? 'Confirm 2:00' : t < 19.3 ? '<span style="width:18px;height:18px;border-radius:50%;border:2.4px solid rgb(255 255 255 / .35);border-top-color:#fff;display:inline-block;animation:spin .8s linear infinite"></span>Booking…' : `${icon.check}Booked for Thursday 2:00`
	})
})
</script>
</section>