← Public packages
@kentcdodds/invisible-software
Invisible software: a phone-only step-through demo plus a chart of AI agent benchmark progress.
src/slides/02-grokbot-ping.html
152 lines · 8.5 KB · HTML<section class="slide uses-vc-app" data-label="2" data-duration="17" data-steps="[[0.4,1.7],[3.1,6.25],[6.55,9.2],[9.6,10],[10.75,12.4]]" data-human="[2,5]">
<style>
@keyframes notifIn { from { opacity: 0; transform: translateY(60px) scale(0.92); } to { opacity: 1; transform: none; } }
@keyframes lockAway { to { transform: scale(0.94); filter: blur(10px) brightness(0.8); opacity: 0; } }
@keyframes openApp { from { clip-path: inset(318px 10px 418px 10px round 24px); opacity: 0; } 30% { opacity: 1; } to { clip-path: inset(0 0 0 0 round 56px); opacity: 1; } }
@keyframes grow-grokbot-ping { from { max-height: 0; } to { max-height: 320px; } }
@keyframes shrink-grokbot-ping { from { max-height: 60px; opacity: 1; } to { max-height: 0; opacity: 0; } }
@scope {
.phone { left: 1150px; top: 102px; transform: scale(1.1); }
.chat { z-index: 20; }
.notif { top: 318px; animation: notifIn 0.7s cubic-bezier(.2,1.25,.4,1) 1.0s both, fadeOut 0.25s ease 3.75s forwards; }
.lock { animation: lockAway 0.55s cubic-bezier(.4,0,.2,1) 3.7s forwards; }
.chat { animation: openApp 0.6s cubic-bezier(.3,.9,.3,1) 3.7s both; }
.row { overflow: hidden; display: flex; flex-direction: column; animation: grow-grokbot-ping 0.4s cubic-bezier(.22,1,.36,1) var(--d) both; }
.row > * { margin-top: 10px; animation: bubbleIn 0.4s cubic-bezier(.22,1,.36,1) var(--d) both; }
.row.me > * { transform-origin: right bottom; } .row > * { transform-origin: left bottom; }
.row.gone { animation: grow-grokbot-ping 0.3s ease var(--d) both, shrink-grokbot-ping 0.25s ease var(--e) forwards; }
.stamp { align-self: center; color: #8a8a8e; font: 500 13.5px/1 var(--ios); margin: 0 0 4px; }
.caption { left: 160px; width: 820px; }
.sub b { color: var(--kody-text); font-weight: 650; }
.linkcard { align-self: flex-start; width: 286px; border-radius: 20px; overflow: hidden; background: #f0f0f2; }
.lc-img { height: 132px; background: var(--vc-cream); padding: 14px 14px 0; position: relative; overflow: hidden; }
.lc-img .app-head .av-img { width: 34px; height: 34px; }
.lc-img .app-head h1 { font-size: 16px; }
.lc-img .app-head p { font-size: 11px; }
.lc-img .mini-row { margin-top: 8px; height: 30px; border-radius: 9px; background: #fff; border: 1px solid #ece5d8; display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 600 11.5px/1 var(--ios); color: var(--vc-ink); }
.lc-img .mini-row .av-img, .lc-img .mini-row .mono { width: 20px; height: 20px; font-size: 8px; }
.lc-img .mini-row .due { margin-left: auto; font: 700 9px/1 var(--ios); padding: 4px 5px; border-radius: 5px; background: #fde8dc; color: #b4451a; letter-spacing: .03em; }
.lc-meta { padding: 10px 14px 12px; }
.lc-meta b { display: block; font: 650 15.5px/1.25 var(--ios); color: #111; }
.lc-meta span { display: flex; align-items: center; gap: 5px; margin-top: 3px; font: 400 13.5px/1.2 var(--ios); color: #8a8a8e; }
.lc-meta span img { width: 15px; height: 15px; }
.safari { z-index: 30; opacity: 0; }
.safari-body { background: var(--vc-cream); }
}
</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 3.9s forwards">
<div class="kicker">Victoria · independent barber</div>
<div class="headline">Grok Bot spots<br>the <em>open chairs</em></div>
<div class="sub">Four open slots Thursday, and regulars who are <b>due for a cut</b>.</div>
</div>
<div class="cap" style="animation: capIn .8s cubic-bezier(.22,1,.36,1) 4.5s both, capOut .5s ease 10.9s forwards">
<div class="kicker">One tap</div>
<div class="headline">She just<br>says <em>yes</em></div>
<div class="sub">Grok Bot builds her a client-finder app on Kody.</div>
</div>
<div class="cap" style="animation: capIn .8s cubic-bezier(.22,1,.36,1) 11.6s both, capOut .5s ease 15.9s forwards">
<div class="kicker">Built on Kody</div>
<div class="headline">Her own app,<br><em>ready to go</em></div>
<div class="sub">victoria.kody.run opens right in Safari.</div>
</div>
</div>
<div class="phone"><div class="screen">
<div class="island"></div>
<div class="chat">
<div class="statusbar"></div>
<div class="chat-head">
<div class="round-btn back" data-icon="chevL"></div>
<div class="head-pill"><div class="av gb-icon"><img src="assets/grokbot.svg"></div>Grok Bot</div>
<div class="round-btn" data-icon="dots"></div>
</div>
<div class="msgs">
<div class="stamp">Today 9:41 AM</div>
<div class="msg bot">You have 4 open slots Thursday. Want me to text regulars who are due for a cut?</div>
<div class="row me" style="--d:7.0s"><div class="msg me">Yes please!</div></div>
<div class="row gone" style="--d:7.5s;--e:8.8s"><div class="typing"><i></i><i></i><i></i></div></div>
<div class="row" style="--d:8.8s"><div class="msg bot">Done! I built you an app in Kody. It finds regulars who are due and sends your Thursday text in one tap.</div></div>
<div class="row" style="--d:9.6s"><div class="linkcard" id="linkCard">
<div class="lc-img">
<div class="app-head"><img class="av-img" src="assets/victoria.jpg"><div><h1>Victoria's Chair</h1><p>Client finder</p></div></div>
<div class="mini-row"><img class="av-img" src="assets/client.jpg">Marcus Reed<span class="due">Due</span></div>
<div class="mini-row"><span class="mono" style="background:#7c8cff">JO</span>Jay Ortiz<span class="due">Due</span></div>
</div>
<div class="lc-meta"><b>Victoria's Chair · Client finder</b><span><img src="assets/kody-mark.png">victoria.kody.run</span></div>
</div></div>
</div>
<div class="composer">
<div class="round-btn plus" data-icon="plus"></div>
<div class="field"><span class="ph">Ask Grok Bot</span><span class="typed caret" style="display:none"></span><span class="mic" data-icon="mic"></span><span class="go" data-icon="wave"></span></div>
</div>
<div class="home-ind"></div>
</div>
<div class="safari" id="saf">
<div class="statusbar"></div>
<div class="safari-top" data-url="victoria.kody.run"></div>
<div class="safari-body"></div>
<div class="safari-bottom"></div>
</div>
<div class="home-ind" style="z-index:60;opacity:0" id="safHome"></div>
<div class="lock">
<div class="statusbar light"></div>
<div class="lock-date">Tuesday, October 13</div>
<div class="lock-time">9:41</div>
<div class="notif">
<div class="app-ic gb-icon"><img src="assets/grokbot.svg"></div>
<div><div class="n-head">Grok Bot<time>now</time></div>
<div class="n-body">You have 4 open slots Thursday. Want me to text regulars who are due for a cut?</div></div>
</div>
<div class="lock-btn l" data-icon="flash"></div>
<div class="lock-btn r" data-icon="camera"></div>
<div class="home-ind light"></div>
</div>
<div class="tap" style="left:196px;top:368px;animation: tap .7s ease 3.1s both"></div>
<div class="tap" style="left:230px;top:788px;animation: tap .6s ease 5.0s both"></div>
<div class="tap" style="left:350px;top:788px;animation: tap .6s ease 6.55s both"></div>
</div></div>
<div class="stage-vignette"></div>
<script>
deck.slide(({ root, onTime, typer, tapOn, mountFinder }) => {
const T_OPEN = 11.4
mountFinder(root.querySelector('.safari-body'), T_OPEN, 1.0)
tapOn('#linkCard', 10.9)
const saf = root.querySelector('#saf')
onTime((t) => {
const o = easeInOut(prog(t, T_OPEN, T_OPEN + 0.6))
saf.style.opacity = t < T_OPEN ? 0 : 1
root.querySelector('.lock').style.visibility = t > 4.4 ? 'hidden' : ''
root.querySelector('.chat').style.visibility = o >= 1 ? 'hidden' : ''
root.querySelector('#safHome').style.opacity = o >= 1 ? 1 : 0
if (t < T_OPEN) return
const screen = root.querySelector('.screen'), card = root.querySelector('#linkCard')
const s = screen.getBoundingClientRect(), r = card.getBoundingClientRect(), k = s.width / screen.offsetWidth
const top = (r.top - s.top) / k, left = (r.left - s.left) / k, right = 393 - (r.right - s.left) / k, bottom = 852 - (r.bottom - s.top) / k
saf.style.clipPath = o >= 1 ? 'none' : `inset(${top * (1 - o)}px ${right * (1 - o)}px ${bottom * (1 - o)}px ${left * (1 - o)}px round ${20 + o * 36}px)`
})
const ph = root.querySelector('.composer .ph')
const typed = root.querySelector('.composer .typed')
const go = root.querySelector('.composer .go')
typer(typed, 'Yes please!', 5.4, 13)
onTime((t) => {
const composing = t >= 5.2 && t < 7.0
ph.style.display = composing ? 'none' : ''
typed.style.display = composing ? '' : 'none'
go.innerHTML = t >= 5.5 && t < 7.0 ? icon.send : icon.wave
})
})
</script>
</section>