feat(landing): desktop-first homepage — download CTA leads, CLI below the fold (#221)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-06 20:08:24 +08:00
committed by GitHub
parent e07106a924
commit 8fc26cc44f
7 changed files with 86 additions and 90 deletions
+15
View File
@@ -0,0 +1,15 @@
/**
* Coarse client-side OS detection, shared by the hero's platform-aware download
* button and the download page's platform cards. Detection only tunes emphasis
* (which label the hero button carries, which card gets badged) — absent or wrong
* detection never blocks a download.
*/
export type Platform = "mac" | "windows" | "linux";
export function detectPlatform(): Platform | null {
const ua = navigator.userAgent;
if (/Windows/i.test(ua)) return "windows";
if (/Macintosh|Mac OS X/i.test(ua)) return "mac";
if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux";
return null;
}
+10 -4
View File
@@ -54,9 +54,14 @@ export const en: Strings = {
titleWords: ["Desktop", "Server"],
titleSuffix: "",
subtitle: "Create Self-Evolving Agents in One Click",
ctaPrimary: "Get started",
/** Primary CTA: base label, and the platform-aware variant once the OS is detected. */
downloadCta: "Download the desktop app",
downloadCtaFor: (platform: string) => `Download for ${platform}`,
ctaQuickstart: "Get started",
ctaGithub: "GitHub",
installHint: "One-line install (bundled Node runtime — unpack and run)",
/** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */
downloadAll: "All platforms (macOS / Windows / Linux)",
cliAlt: "CLI and self-hosted install ↓",
stats: [
{ value: "1000+", label: "supported models" },
{ value: "1×CPU", label: "minimum footprint" },
@@ -80,7 +85,7 @@ export const en: Strings = {
},
offlineRelease: "Download offline packages from GitHub Releases",
desktopNote:
"Prefer a standalone app? The desktop app packages the Web interface and the server into a terminal-free installer (macOS / Windows / Linux), sharing the same local data as a CLI install.",
"Already on the desktop app? A CLI install shares the same local data with it, and the two can be used side by side.",
desktopPage: "Go to the desktop download page",
},
@@ -485,7 +490,8 @@ export const en: Strings = {
title: "Complex AI development, made ever simpler",
subtitle:
"Through continuous evolution, PenguinHarness gives you a more efficient, more reliable, lower-hallucination and lower-cost Agent productivity engine.",
install: "Install now",
download: "Download the desktop app",
quickstart: "Get started",
docs: "Read the docs",
},
+11 -6
View File
@@ -59,9 +59,14 @@ export const zh = {
titleWords: ["桌面", "服务器"],
titleSuffix: "上",
subtitle: "一键创建自进化 Agent",
ctaPrimary: "快速开始",
/** Primary CTA: base label, and the platform-aware variant once the OS is detected. */
downloadCta: "下载桌面版",
downloadCtaFor: (platform: string) => `下载桌面版(${platform})`,
ctaQuickstart: "快速开始",
ctaGithub: "GitHub",
installHint: "一行命令安装(内嵌 Node 运行时,解压即用)",
/** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */
downloadAll: "全部平台下载(macOS / Windows / Linux)",
cliAlt: "命令行与自托管安装 ↓",
stats: [
{ value: "1000+", label: "支持模型数量" },
{ value: "1×CPU", label: "最低运行配置" },
@@ -70,7 +75,7 @@ export const zh = {
],
},
/** Install-method switcher (hero + quick start): OS tabs, online/offline methods. */
/** Install-method switcher (quick start step 1): OS tabs, online/offline methods. */
install: {
linux: "Linux",
macos: "macOS",
@@ -85,8 +90,7 @@ export const zh = {
windows: "解压后也可直接双击 install.cmd 完成安装。",
},
offlineRelease: "前往 GitHub Releases 下载离线安装包",
desktopNote:
"更喜欢独立应用?桌面端把 Web 界面与服务端打包成免终端的安装程序(macOS / Windows / Linux),与 CLI 安装共用同一份本地数据。",
desktopNote: "已经装了桌面版?CLI 安装与桌面版共用同一份本地数据,两者可以随时并用。",
desktopPage: "前往桌面端下载页",
},
@@ -475,7 +479,8 @@ export const zh = {
title: "让复杂的 AI 开发越来越简单",
subtitle:
"通过不断进化,PenguinHarness 为你提供更高效、更可靠、更低幻觉、更低成本的 Agent 生产力引擎。",
install: "立即安装",
download: "下载桌面版",
quickstart: "快速开始",
docs: "阅读文档",
},
+2 -10
View File
@@ -23,22 +23,14 @@ import {
OSS_ORIGIN,
RELEASES_URL,
} from "../lib/links";
import { detectPlatform } from "../lib/platform";
import type { Platform } from "../lib/platform";
import { Section } from "../components/section";
import { CodeCard } from "../components/code-card";
import { ChevronDownIcon, DownloadIcon, ExternalLinkIcon } from "../components/icons";
type Platform = "mac" | "windows" | "linux";
const PLATFORMS: Platform[] = ["mac", "windows", "linux"];
/** Coarse OS detection, used only to badge one card; absent or wrong detection changes nothing else. */
function detectPlatform(): Platform | null {
const ua = navigator.userAgent;
if (/Windows/i.test(ua)) return "windows";
if (/Macintosh|Mac OS X/i.test(ua)) return "mac";
if (/Linux|X11/.test(ua) && !/Android/i.test(ua)) return "linux";
return null;
}
interface Mirror {
tag: string;
base: string;
+11 -9
View File
@@ -1,11 +1,13 @@
/** Closing call-to-action: the productivity-engine pitch + install / docs buttons. */
/** Closing call-to-action: the productivity-engine pitch + download / quick start / docs buttons. */
import { Link } from "react-router";
import { S } from "../lib/strings";
import { DOCS_URL } from "../lib/links";
import { Section } from "../components/section";
import { ArrowRightIcon } from "../components/icons";
import { DownloadIcon } from "../components/icons";
export function Cta() {
const secondary =
"inline-flex h-11 items-center rounded-lg border border-gray-200 bg-white px-5 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800";
return (
<Section>
<div className="mx-auto max-w-3xl rounded-2xl border border-gray-200 bg-gray-50/60 px-6 py-12 text-center sm:px-12 dark:border-gray-800 dark:bg-gray-900/40">
@@ -17,16 +19,16 @@ export function Cta() {
</p>
<div className="mt-7 flex flex-wrap items-center justify-center gap-3">
<Link
to="/#quickstart"
to="/download"
className="inline-flex h-11 items-center gap-2 rounded-lg bg-gray-900 px-5 text-sm font-medium text-white transition-colors hover:bg-gray-700 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
>
{S.cta.install}
<ArrowRightIcon className="h-4 w-4" />
<DownloadIcon className="h-4 w-4" />
{S.cta.download}
</Link>
<a
href={DOCS_URL}
className="inline-flex h-11 items-center rounded-lg border border-gray-200 bg-white px-5 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800"
>
<Link to="/#quickstart" className={secondary}>
{S.cta.quickstart}
</Link>
<a href={DOCS_URL} className={secondary}>
{S.cta.docs}
</a>
</div>
+34 -59
View File
@@ -1,66 +1,20 @@
/**
* Hero: enlarged logo + product name, the one-line headline whose rotating word
* crossfades through a gaussian blur (Desktop <-> Server, localized per dictionary),
* the one-sentence subtitle, the install one-liner behind an OS switcher, and stats.
* the desktop-first CTAs — a platform-aware download button pointing at /download
* (artifact resolution stays on that page), an all-platforms line and a link down to
* the quick start for CLI / self-hosted installs — and stats.
* The rotating word is a stacked inline-grid so line width never jumps.
*/
import { useEffect, useState } from "react";
import { Link } from "react-router";
import { S } from "../lib/strings";
import { INSTALL_CMD, INSTALL_CMD_WINDOWS, REPO_URL } from "../lib/links";
import { CopyButton } from "../components/copy-button";
import { ArrowRightIcon, GitHubIcon } from "../components/icons";
import { REPO_URL } from "../lib/links";
import { detectPlatform } from "../lib/platform";
import { ArrowRightIcon, DownloadIcon, GitHubIcon } from "../components/icons";
const ROTATE_MS = 2600;
type InstallOs = "linux" | "macos" | "windows";
/**
* Install one-liner behind an OS tab row: one command visible at a time (Linux and
* macOS share the POSIX one-liner; the tabs still name them apart so nobody has to
* know that). Prompt char follows the shell: $ for POSIX, > for PowerShell.
*/
function InstallBox() {
const [os, setOs] = useState<InstallOs>("linux");
const cmd = os === "windows" ? INSTALL_CMD_WINDOWS : INSTALL_CMD;
const osBtn = (active: boolean) =>
`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
active
? "bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-gray-100"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
}`;
return (
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 text-left dark:border-gray-800 dark:bg-gray-900">
<div
className="flex items-center gap-1 border-b border-gray-200 bg-gray-100/70 px-2 py-1.5 dark:border-gray-800 dark:bg-gray-800/40"
role="tablist"
>
{(["linux", "macos", "windows"] as const).map((key) => (
<button
key={key}
type="button"
role="tab"
aria-selected={os === key}
className={osBtn(os === key)}
onClick={() => setOs(key)}
>
{S.install[key]}
</button>
))}
</div>
<div className="flex items-center gap-3 py-2.5 pr-2.5 pl-4">
<code className="min-w-0 flex-1 overflow-x-auto font-mono text-[13px] whitespace-nowrap text-gray-800 dark:text-gray-200">
<span className="mr-2 text-gray-400 select-none dark:text-gray-500">
{os === "windows" ? ">" : "$"}
</span>
{cmd}
</code>
<CopyButton text={cmd} className="shrink-0" />
</div>
</div>
);
}
function RotatingWord({ words }: { words: string[] }) {
const [active, setActive] = useState(0);
useEffect(() => {
@@ -86,6 +40,15 @@ function RotatingWord({ words }: { words: string[] }) {
}
export function Hero() {
// Platform-aware label only; every link goes to /download, where the platform
// cards and the GitHub/OSS artifact resolution live.
const detected = detectPlatform();
const downloadLabel = detected
? S.hero.downloadCtaFor(S.download.platforms[detected].name)
: S.hero.downloadCta;
const textLink =
"inline-flex items-center gap-1 text-brand-700 underline decoration-brand-300 underline-offset-2 transition-colors hover:text-brand-600 dark:text-brand-300 dark:decoration-brand-700";
return (
<section className="relative overflow-hidden">
<div className="hero-dots pointer-events-none absolute inset-0" aria-hidden="true" />
@@ -125,10 +88,17 @@ export function Hero() {
style={{ animationDelay: "200ms" }}
>
<Link
to="/#quickstart"
to="/download"
className="inline-flex h-11 items-center gap-2 rounded-lg bg-gray-900 px-5 text-sm font-medium text-white transition-colors hover:bg-gray-700 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
>
{S.hero.ctaPrimary}
<DownloadIcon className="h-4 w-4" />
{downloadLabel}
</Link>
<Link
to="/#quickstart"
className="inline-flex h-11 items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800"
>
{S.hero.ctaQuickstart}
<ArrowRightIcon className="h-4 w-4" />
</Link>
<a
@@ -142,13 +112,18 @@ export function Hero() {
</a>
</div>
<div
className="anim-rise mx-auto mt-10 w-fit max-w-full"
<p
className="anim-rise mt-5 text-sm text-gray-600 dark:text-gray-400"
style={{ animationDelay: "240ms" }}
>
<InstallBox />
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">{S.hero.installHint}</p>
</div>
<Link to="/download" className={textLink}>
{S.hero.downloadAll}
</Link>
<span className="mx-2 text-gray-300 dark:text-gray-700">·</span>
<Link to="/#quickstart" className={textLink}>
{S.hero.cliAlt}
</Link>
</p>
<dl
className="anim-rise mx-auto mt-12 grid max-w-3xl grid-cols-2 gap-x-6 gap-y-8 sm:grid-cols-4"
+3 -2
View File
@@ -200,8 +200,9 @@ export function Quickstart() {
</p>
</>
)}
{/* Below both install methods: the desktop app replaces this whole flow for
users who never want a terminal, so it stays visible on every switch. */}
{/* Below both install methods: the desktop app is the headline install path
(hero + /download); this note tells CLI installers the two share one data
root, and still routes anyone terminal-averse back to the download page. */}
<p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.install.desktopNote}{" "}
<Link