'use client'; import { ArrowLeft, Bot, Coffee, Download as DownloadIcon, FileText, Github, } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons'; import { Button } from '@/components/ui/button'; import { DONATE_URL, GITHUB_RELEASES_PAGE, GITHUB_REPO } from '@/lib/constants'; import type { DownloadLinks } from '@/lib/releases'; type Platform = keyof DownloadLinks; type PlatformMeta = { key: Platform; label: string; description: string; icon: React.ComponentType<{ className?: string }>; }; const PLATFORMS: PlatformMeta[] = [ { key: 'macArm', label: 'macOS', description: 'Apple Silicon', icon: AppleIcon }, { key: 'macIntel', label: 'macOS', description: 'Intel (x64)', icon: AppleIcon }, { key: 'windows', label: 'Windows', description: '64-bit (MSI)', icon: WindowsIcon }, { key: 'linux', label: 'Linux', description: 'Build from source', icon: LinuxIcon }, ]; function detectPlatform(): Platform | null { if (typeof navigator === 'undefined') return null; const ua = navigator.userAgent; if (/Windows/i.test(ua)) return 'windows'; if (/Linux/i.test(ua) && !/Android/i.test(ua)) return 'linux'; if (/Mac/i.test(ua)) { // Apple Silicon Safari reports "Intel" for compat; default to ARM since // M-series is the majority. Users can click the Intel button if needed. return 'macArm'; } return null; } function parseQueryPlatform(search: string): Platform | null { const params = new URLSearchParams(search); const raw = params.get('platform'); if (!raw) return null; // Accept both camelCase and hyphenated forms (/download/mac-arm → ?platform=mac-arm). const normalized = raw .toLowerCase() .replace(/[-_\s]/g, '') .replace('macarm', 'macArm') .replace('macintel', 'macIntel'); const valid: Platform[] = ['macArm', 'macIntel', 'windows', 'linux']; return (valid as string[]).includes(normalized) ? (normalized as Platform) : null; } export default function DownloadPage() { const [links, setLinks] = useState(null); const [linksError, setLinksError] = useState(false); const [platform, setPlatform] = useState(null); const [triggered, setTriggered] = useState(false); useEffect(() => { const fromQuery = parseQueryPlatform(window.location.search); const resolved = fromQuery ?? detectPlatform(); // No prebuilt Linux binary yet — send Linux users to the build-from-source // instructions instead of sitting on /download trying to trigger a // download that doesn't exist. if (resolved === 'linux') { window.location.replace('/linux-install'); return; } setPlatform(resolved); }, []); useEffect(() => { let cancelled = false; fetch('/api/releases') .then((r) => { if (!r.ok) throw new Error(`releases ${r.status}`); return r.json(); }) .then((data) => { if (cancelled) return; if (data.downloadLinks) setLinks(data.downloadLinks as DownloadLinks); }) .catch(() => { if (!cancelled) setLinksError(true); }); return () => { cancelled = true; }; }, []); useEffect(() => { if (triggered || !links || !platform) return; const url = links[platform]; if (!url) return; const a = document.createElement('a'); a.href = url; a.rel = 'noopener'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTriggered(true); }, [triggered, links, platform]); const activeMeta = useMemo( () => PLATFORMS.find((p) => p.key === platform) ?? null, [platform], ); return (
{/* Minimal branded header */}
Voicebox Voicebox Back to voicebox.sh
{/* Hero */}
Voicebox
{triggered ? ( <>

Your download has started.

{activeMeta ? `Downloading Voicebox for ${activeMeta.label} (${activeMeta.description}). Check your downloads folder.` : 'Check your downloads folder for Voicebox.'}

) : ( <>

{linksError ? "We couldn't load the latest release." : 'Download Voicebox'}

{linksError ? 'Our release server is temporarily unreachable. Please try again in a moment.' : 'Pick your platform to get started.'}

)}
{/* Platform buttons — always visible as a fallback */} {linksError ? (

If this keeps happening, you can{' '} browse releases on GitHub {' '}and grab the build for your platform manually.

) : (

{triggered ? 'Download not working?' : 'Choose your platform'}

)} {/* Donate — prominent, heartfelt, post-click context */}
Hi from the maintainer

Jamie here — Voicebox is a side project.

I build and maintain Voicebox in my spare time. It's completely free, open source, runs entirely on your machine — no accounts, no cloud, no subscriptions, no upsells. If it saves you an ElevenLabs bill or just made your day, a coffee genuinely helps me keep shipping updates, adding new models, and fixing bugs. Every little bit keeps the lights on.

{/* Resources */}
); }