feat: add multi-platform downloads and improve homepage animations
This commit is contained in:
@@ -1,11 +1,65 @@
|
||||
import * as React from 'react'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { CursorFieldProvider } from '~/components/butterfly'
|
||||
import websitePackage from '../../package.json'
|
||||
import '~/styles.css'
|
||||
|
||||
const desktopVersion = websitePackage.version
|
||||
const macDownloadHref = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}/Paseo_${desktopVersion}_aarch64.dmg`
|
||||
const releaseBase = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}`
|
||||
|
||||
type Platform = 'mac-silicon' | 'mac-intel' | 'windows' | 'linux'
|
||||
|
||||
interface DownloadOption {
|
||||
platform: Platform
|
||||
label: string
|
||||
href: string
|
||||
icon: (props: React.SVGProps<SVGSVGElement>) => React.ReactElement
|
||||
}
|
||||
|
||||
const downloadOptions: DownloadOption[] = [
|
||||
{
|
||||
platform: 'mac-silicon',
|
||||
label: 'Mac',
|
||||
href: `${releaseBase}/Paseo_${desktopVersion}_aarch64.dmg`,
|
||||
icon: AppleIcon,
|
||||
},
|
||||
{
|
||||
platform: 'mac-intel',
|
||||
label: 'Mac Intel',
|
||||
href: `${releaseBase}/Paseo_${desktopVersion}_x86_64.dmg`,
|
||||
icon: AppleIcon,
|
||||
},
|
||||
{
|
||||
platform: 'windows',
|
||||
label: 'Windows',
|
||||
href: `${releaseBase}/Paseo_${desktopVersion}_x64-setup.exe`,
|
||||
icon: WindowsIcon,
|
||||
},
|
||||
{
|
||||
platform: 'linux',
|
||||
label: 'Linux',
|
||||
href: `${releaseBase}/Paseo_${desktopVersion}_amd64.AppImage`,
|
||||
icon: LinuxIcon,
|
||||
},
|
||||
]
|
||||
|
||||
function useDetectedPlatform(): Platform {
|
||||
const [platform, setPlatform] = React.useState<Platform>('mac-silicon')
|
||||
|
||||
React.useEffect(() => {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
if (ua.includes('win')) {
|
||||
setPlatform('windows')
|
||||
} else if (ua.includes('linux') && !ua.includes('android')) {
|
||||
setPlatform('linux')
|
||||
} else if (ua.includes('mac')) {
|
||||
setPlatform('mac-silicon')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return platform
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
head: () => ({
|
||||
@@ -36,7 +90,12 @@ function Home() {
|
||||
</div>
|
||||
|
||||
{/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */}
|
||||
<div className="relative px-6 md:px-8 pb-8 md:pb-16">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5, ease: 'easeOut' }}
|
||||
className="relative px-6 md:px-8 pb-8 md:pb-16"
|
||||
>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<img
|
||||
src="/paseo-mockup.png"
|
||||
@@ -44,7 +103,7 @@ function Home() {
|
||||
className="w-full rounded-lg shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Content section */}
|
||||
@@ -114,16 +173,53 @@ function Nav() {
|
||||
function Hero() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl md:text-5xl font-bold tracking-tight">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||
className="text-3xl md:text-5xl font-semibold tracking-tight"
|
||||
>
|
||||
One interface for all your coding agents
|
||||
</h1>
|
||||
<p className="text-white/70 text-lg leading-relaxed">
|
||||
Run Claude Code, Codex, and OpenCode on your machine. Connect from your phone or desktop.
|
||||
</p>
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: 'easeOut' }}
|
||||
className="text-white/70 text-lg leading-relaxed max-w-lg"
|
||||
>
|
||||
Run agents in parallel on your own machines. Ship from your phone or your desk.
|
||||
</motion.p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
|
||||
const [hovered, setHovered] = React.useState(false)
|
||||
|
||||
return (
|
||||
<span
|
||||
className="relative inline-flex items-center justify-center rounded-full p-1.5 text-white/60"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
{icon}
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 4 }}
|
||||
transition={{ duration: 0.15, ease: 'easeOut' }}
|
||||
className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded bg-white text-black text-xs whitespace-nowrap pointer-events-none"
|
||||
>
|
||||
{name}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Differentiator({ title, description }: { title: string; description: string }) {
|
||||
return (
|
||||
<div>
|
||||
@@ -137,108 +233,246 @@ function Features() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<Feature
|
||||
title="Self-hosted"
|
||||
description="The daemon runs on your laptop, home server, or VPS. Allowing you to take full advantage of your dev environment."
|
||||
/>
|
||||
<Feature
|
||||
title="Multi-provider"
|
||||
description="Works with existing agent harnesses like Claude Code, Codex, and OpenCode from one interface."
|
||||
/>
|
||||
<Feature
|
||||
title="Multi-host"
|
||||
description="Connect to multiple daemons and see all your agents in one place."
|
||||
/>
|
||||
<Feature
|
||||
title="First-class voice"
|
||||
description="Real-time voice conversations and dictation. Talk to your agent, hear responses, and orchestrate work hands-free."
|
||||
/>
|
||||
<Feature
|
||||
title="Optional relay"
|
||||
description="Use the hosted end-to-end encrypted relay for remote access, or connect directly over your network."
|
||||
/>
|
||||
<Feature
|
||||
title="Cross-device"
|
||||
description="Jump seamlessly between iOS, Android, desktop, web, and CLI."
|
||||
/>
|
||||
<Feature
|
||||
title="Git integration"
|
||||
description="Manage agents in isolated worktrees. Review diffs and ship directly from the app."
|
||||
/>
|
||||
<Feature
|
||||
title="Open source"
|
||||
description="Free and open source. Run it yourself, fork it, contribute."
|
||||
/>
|
||||
{[
|
||||
{ title: 'Self-hosted', description: 'The daemon runs on your laptop, home server, or VPS. Allowing you to take full advantage of your dev environment.' },
|
||||
{ title: 'Multi-provider', description: 'Works with existing agent harnesses like Claude Code, Codex, and OpenCode from one interface.' },
|
||||
{ title: 'Multi-host', description: 'Connect to multiple daemons and see all your agents in one place.' },
|
||||
{ title: 'First-class voice', description: 'Real-time voice conversations and dictation. Talk to your agent, hear responses, and orchestrate work hands-free.' },
|
||||
{ title: 'Optional relay', description: 'Use the hosted end-to-end encrypted relay for remote access, or connect directly over your network.' },
|
||||
{ title: 'Cross-device', description: 'Jump seamlessly between iOS, Android, desktop, web, and CLI.' },
|
||||
{ title: 'Git integration', description: 'Manage agents in isolated worktrees. Review diffs and ship directly from the app.' },
|
||||
{ title: 'Open source', description: 'Free and open source. Run it yourself, fork it, contribute.' },
|
||||
].map((feature, i) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-40px' }}
|
||||
transition={{ duration: 0.4, delay: i * 0.06, ease: 'easeOut' }}
|
||||
className="space-y-1"
|
||||
>
|
||||
<p className="font-medium text-base">{feature.title}</p>
|
||||
<p className="text-sm text-white/60">{feature.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Feature({ title, description }: { title: string; description: string }) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-base">{title}</p>
|
||||
<p className="text-sm text-white/60">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GetStarted() {
|
||||
return (
|
||||
<div className="pt-10 space-y-6">
|
||||
<Step number={1}>
|
||||
<p className="text-sm text-white/70">Install the daemon</p>
|
||||
<CodeBlock>npm install -g @getpaseo/cli && paseo</CodeBlock>
|
||||
</Step>
|
||||
<Step number={2}>
|
||||
<p className="text-sm text-white/70">Download any app</p>
|
||||
<div className="flex flex-row gap-3">
|
||||
<a
|
||||
href={macDownloadHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<AppleIcon className="h-4 w-4" />
|
||||
Download for Mac
|
||||
</a>
|
||||
<a
|
||||
href="https://app.paseo.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<GlobeIcon className="h-4 w-4" />
|
||||
Launch Web App
|
||||
</a>
|
||||
<span className="relative group inline-flex items-center justify-center rounded-lg border border-white/10 px-3 py-2 text-white/40 cursor-default">
|
||||
<AppleIcon className="h-5 w-5" />
|
||||
<span className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded bg-white text-black text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
Coming soon
|
||||
</span>
|
||||
</span>
|
||||
<span className="relative group inline-flex items-center justify-center rounded-lg border border-white/10 px-3 py-2 text-white/40 cursor-default">
|
||||
<GooglePlayIcon className="h-5 w-5 opacity-40" />
|
||||
<span className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded bg-white text-black text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
Coming soon
|
||||
</span>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: 'easeOut' }}
|
||||
className="pt-10"
|
||||
>
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
<DownloadButton />
|
||||
<a
|
||||
href="https://app.paseo.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<GlobeIcon className="h-4 w-4" />
|
||||
Launch Web App
|
||||
</a>
|
||||
<a
|
||||
href="https://apps.apple.com/app/paseo-pocket-engineer/id6758887924"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
|
||||
aria-label="App Store"
|
||||
>
|
||||
<AppleIcon className="h-5 w-5" />
|
||||
</a>
|
||||
<span className="relative group inline-flex items-center justify-center rounded-lg border border-white/10 px-3 py-2 text-white/40 cursor-default">
|
||||
<GooglePlayIcon className="h-5 w-5 opacity-40" />
|
||||
<span className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded bg-white text-black text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||
Coming soon
|
||||
</span>
|
||||
</span>
|
||||
<ServerInstallButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-8">
|
||||
<span className="text-xs text-white/40">Supports</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<AgentBadge name="Claude Code" icon={<ClaudeCodeIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="Codex" icon={<CodexIcon className="h-6 w-6" />} />
|
||||
<AgentBadge name="OpenCode" icon={<OpenCodeIcon className="h-6 w-6" />} />
|
||||
</div>
|
||||
</Step>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function DownloadButton() {
|
||||
const detectedPlatform = useDetectedPlatform()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const primary = downloadOptions.find((o) => o.platform === detectedPlatform)!
|
||||
const secondaryOptions = downloadOptions.filter((o) => o.platform !== detectedPlatform)
|
||||
const PrimaryIcon = primary.icon
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<div className="flex items-stretch">
|
||||
<a
|
||||
href={primary.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-l-lg bg-foreground px-4 py-2 text-sm font-medium text-background hover:bg-foreground/90 transition-colors"
|
||||
>
|
||||
<PrimaryIcon className="h-4 w-4" />
|
||||
Download for {primary.label}
|
||||
</a>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="inline-flex items-center justify-center rounded-r-lg border-l border-background/20 bg-foreground px-2 py-2 text-background hover:bg-foreground/90 transition-colors"
|
||||
aria-label="More download options"
|
||||
>
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.15, ease: 'easeOut' }}
|
||||
className="absolute left-0 top-full mt-1 z-50 min-w-[200px] rounded-lg border border-white/20 bg-black/90 backdrop-blur-sm py-1"
|
||||
>
|
||||
{secondaryOptions.map((option) => {
|
||||
const Icon = option.icon
|
||||
return (
|
||||
<a
|
||||
key={option.platform}
|
||||
href={option.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-white/80 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{option.label}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerInstallButton() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
|
||||
aria-label="Install on a server"
|
||||
>
|
||||
<TerminalIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-base font-medium text-white">Run agents on a remote machine</p>
|
||||
<p className="text-sm text-white/60">
|
||||
For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon.
|
||||
</p>
|
||||
</div>
|
||||
<CodeBlock>npm install -g @getpaseo/cli && paseo</CodeBlock>
|
||||
<p className="text-xs text-white/30">
|
||||
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to start the daemon.
|
||||
</p>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ClaudeCodeIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fillRule="evenodd" aria-hidden="true" {...props}>
|
||||
<path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CodexIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fillRule="evenodd" aria-hidden="true" {...props}>
|
||||
<path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function OpenCodeIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="96 64 288 384" fill="currentColor" aria-hidden="true" {...props}>
|
||||
<path d="M320 224V352H192V224H320Z" opacity="0.4" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function AppleIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1408 1664"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M1393 1215q-39 125-123 250q-129 196-257 196q-49 0-140-32q-86-32-151-32q-61 0-142 33q-81 34-132 34q-152 0-301-259Q0 1144 0 902q0-228 113-374q113-144 284-144q72 0 177 30q104 30 138 30q45 0 143-34q102-34 173-34q119 0 213 65q52 36 104 100q-79 67-114 118q-65 94-65 207q0 124 69 223t158 126M1017 42q0 61-29 136q-30 75-93 138q-54 54-108 72q-37 11-104 17q3-149 78-257Q835 41 1011 0q1 3 2.5 11t2.5 11q0 4 .5 10t.5 10" />
|
||||
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -303,6 +537,72 @@ function GooglePlayIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
function WindowsIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M0,0H11.377V11.372H0ZM12.623,0H24V11.372H12.623ZM0,12.623H11.377V24H0Zm12.623,0H24V24H12.623" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function LinuxIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M8.996 4.497c.104-.076.1-.168.186-.158s.022.102-.098.207c-.12.104-.308.243-.46.323-.291.152-.631.336-.993.336s-.647-.167-.853-.33c-.102-.082-.186-.162-.248-.221-.11-.086-.096-.207-.052-.204.075.01.087.109.134.153.064.06.144.137.241.214.195.154.454.304.778.304s.702-.19.932-.32c.13-.073.297-.204.433-.304M7.34 3.781c.055-.02.123-.031.174-.003.011.006.024.021.02.034-.012.038-.074.032-.11.05-.032.017-.057.052-.093.054-.034 0-.086-.012-.09-.046-.007-.044.058-.072.1-.089m.581-.003c.05-.028.119-.018.173.003.041.017.106.045.1.09-.004.033-.057.046-.09.045-.036-.002-.062-.037-.093-.053-.036-.019-.098-.013-.11-.051-.004-.013.008-.028.02-.034" />
|
||||
<path fillRule="evenodd" d="M8.446.019c2.521.003 2.38 2.66 2.364 4.093-.01.939.509 1.574 1.04 2.244.474.56 1.095 1.38 1.45 2.32.29.765.402 1.613.115 2.465a.8.8 0 0 1 .254.152l.001.002c.207.175.271.447.329.698.058.252.112.488.224.615.344.382.494.667.48.922-.015.254-.203.43-.435.57-.465.28-1.164.491-1.586 1.002-.443.527-.99.83-1.505.871a1.25 1.25 0 0 1-1.256-.716v-.001a1 1 0 0 1-.078-.21c-.67.038-1.252-.165-1.718-.128-.687.038-1.116.204-1.506.206-.151.331-.445.547-.808.63-.5.114-1.126 0-1.743-.324-.577-.306-1.31-.278-1.85-.39-.27-.057-.51-.157-.626-.384-.116-.226-.095-.538.07-.988.051-.16.012-.398-.026-.648a2.5 2.5 0 0 1-.037-.369c0-.133.022-.265.087-.386v-.002c.14-.266.368-.377.577-.451s.397-.125.53-.258c.143-.15.27-.374.443-.56q.036-.037.073-.07c-.081-.538.007-1.105.192-1.662.393-1.18 1.223-2.314 1.811-3.014.502-.713.65-1.287.701-2.016.042-.997-.705-3.974 2.112-4.2q.168-.015.321-.013m2.596 10.866-.03.016c-.223.121-.348.337-.427.656-.08.32-.107.733-.13 1.206v.001c-.023.37-.192.824-.31 1.267s-.176.862-.036 1.128v.002c.226.452.608.636 1.051.601s.947-.304 1.36-.795c.474-.576 1.218-.796 1.638-1.05.21-.126.324-.242.333-.4.009-.157-.097-.403-.425-.767-.17-.192-.217-.462-.274-.71-.056-.247-.122-.468-.26-.585l-.001-.001c-.18-.157-.356-.17-.565-.164q-.069.001-.14.005c-.239.275-.805.612-1.197.508-.359-.09-.562-.508-.587-.918m-7.204.03H3.83c-.189.002-.314.09-.44.225-.149.158-.276.382-.445.56v.002h-.002c-.183.184-.414.239-.61.31-.195.069-.353.143-.46.35v.002c-.085.155-.066.378-.029.624.038.245.096.507.018.746v.002l-.001.002c-.157.427-.155.678-.082.822.074.143.235.22.48.272.493.103 1.26.069 1.906.41.583.305 1.168.404 1.598.305.431-.098.712-.369.75-.867v-.002c.029-.292-.195-.673-.485-1.052-.29-.38-.633-.752-.795-1.09v-.002l-.61-1.11c-.21-.286-.43-.462-.68-.5a1 1 0 0 0-.106-.008M9.584 4.85c-.14.2-.386.37-.695.467-.147.048-.302.17-.495.28a1.3 1.3 0 0 1-.74.19.97.97 0 0 1-.582-.227c-.14-.113-.25-.237-.394-.322a3 3 0 0 1-.192-.126c-.063 1.179-.85 2.658-1.226 3.511a5.4 5.4 0 0 0-.43 1.917c-.68-.906-.184-2.066.081-2.568.297-.55.343-.701.27-.649-.266.436-.685 1.13-.848 1.844-.085.372-.1.749.01 1.097.11.349.345.67.766.931.573.351.963.703 1.193 1.015s.302.584.23.777a.4.4 0 0 1-.212.22.7.7 0 0 1-.307.056l.184.235c.094.124.186.249.266.375 1.179.805 2.567.496 3.568-.218.1-.342.197-.664.212-.903.024-.474.05-.896.136-1.245s.244-.634.53-.791a1 1 0 0 1 .138-.061q.005-.045.013-.087c.082-.546.569-.572 1.18-.303.588.266.81.499.71.814h.13c.122-.398-.133-.69-.822-1.025l-.137-.06a2.35 2.35 0 0 0-.012-1.113c-.188-.79-.704-1.49-1.098-1.838-.072-.003-.065.06.081.203.363.333 1.156 1.532.727 2.644a1.2 1.2 0 0 0-.342-.043c-.164-.907-.543-1.66-.735-2.014-.359-.668-.918-2.036-1.158-2.983M7.72 3.503a1 1 0 0 0-.312.053c-.268.093-.447.286-.559.391-.022.021-.05.04-.119.091s-.172.126-.321.238q-.198.151-.13.38c.046.15.192.325.459.476.166.098.28.23.41.334a1 1 0 0 0 .215.133.9.9 0 0 0 .298.066c.282.017.49-.068.673-.173s.34-.233.518-.29c.365-.115.627-.345.709-.564a.37.37 0 0 0-.01-.309c-.048-.096-.148-.187-.318-.257h-.001c-.354-.151-.507-.162-.705-.29-.321-.207-.587-.28-.807-.279m-.89-1.122h-.025a.4.4 0 0 0-.278.135.76.76 0 0 0-.191.334 1.2 1.2 0 0 0-.051.445v.001c.01.162.041.299.102.436.05.116.109.204.183.274l.089-.065.117-.09-.023-.018a.4.4 0 0 1-.11-.161.7.7 0 0 1-.054-.22v-.01a.7.7 0 0 1 .014-.234.4.4 0 0 1 .08-.179q.056-.069.126-.073h.013a.18.18 0 0 1 .123.05c.045.04.08.09.11.162a.7.7 0 0 1 .054.22v.01a.7.7 0 0 1-.002.17 1.1 1.1 0 0 1 .317-.143 1.3 1.3 0 0 0 .002-.194V3.23a1.2 1.2 0 0 0-.102-.437.8.8 0 0 0-.227-.31.4.4 0 0 0-.268-.102m1.95-.155a.63.63 0 0 0-.394.14.9.9 0 0 0-.287.376 1.2 1.2 0 0 0-.1.51v.015q0 .079.01.152c.114.027.278.074.406.138a1 1 0 0 1-.011-.172.8.8 0 0 1 .058-.278.5.5 0 0 1 .139-.2.26.26 0 0 1 .182-.069.26.26 0 0 1 .178.081c.055.054.094.12.124.21.029.086.042.17.04.27l-.002.012a.8.8 0 0 1-.057.277c-.024.059-.089.106-.122.145.046.016.09.03.146.052a5 5 0 0 1 .248.102 1.2 1.2 0 0 0 .244-.763 1.2 1.2 0 0 0-.11-.495.9.9 0 0 0-.294-.37.64.64 0 0 0-.39-.133z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function TerminalIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<polyline points="4 17 10 11 4 5" />
|
||||
<line x1="12" x2="20" y1="19" y2="19" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function GlobeIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -384,7 +684,13 @@ function CodeBlock({ children }: { children: React.ReactNode }) {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="pt-16 space-y-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
className="pt-16 space-y-4"
|
||||
>
|
||||
<h2 className="text-2xl font-medium">Background</h2>
|
||||
<div className="space-y-4 text-sm text-white/60">
|
||||
<p>
|
||||
@@ -403,13 +709,19 @@ function Story() {
|
||||
testing different agents, so locking myself to a single harness or model wasn't an option.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function FAQ() {
|
||||
return (
|
||||
<div className="pt-16 space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
className="pt-16 space-y-6"
|
||||
>
|
||||
<h2 className="text-2xl font-medium">FAQ</h2>
|
||||
<div className="space-y-6">
|
||||
<FAQItem question="Is this free?">
|
||||
@@ -433,7 +745,7 @@ function FAQ() {
|
||||
</FAQItem>
|
||||
<FAQItem question="What does Paseo mean?">Stroll, in Spanish. 🚶♂️</FAQItem>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user