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 ZhengandClaude Fable 5 authored and GitHub committed 2026-08-06 20:08:24 +08:00
1 parent e07106a924
commit 8fc26cc44f
7 files changed
+86 -90

No files matched your search

+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"], titleWords: ["Desktop", "Server"],
titleSuffix: "", titleSuffix: "",
subtitle: "Create Self-Evolving Agents in One Click", 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", 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: [ stats: [
{ value: "1000+", label: "supported models" }, { value: "1000+", label: "supported models" },
{ value: "1×CPU", label: "minimum footprint" }, { value: "1×CPU", label: "minimum footprint" },
@@ -80,7 +85,7 @@ export const en: Strings = {
}, },
offlineRelease: "Download offline packages from GitHub Releases", offlineRelease: "Download offline packages from GitHub Releases",
desktopNote: 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", desktopPage: "Go to the desktop download page",
}, },
@@ -485,7 +490,8 @@ export const en: Strings = {
title: "Complex AI development, made ever simpler", title: "Complex AI development, made ever simpler",
subtitle: subtitle:
"Through continuous evolution, PenguinHarness gives you a more efficient, more reliable, lower-hallucination and lower-cost Agent productivity engine.", "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", docs: "Read the docs",
}, },
+11 -6
View File
@@ -59,9 +59,14 @@ export const zh = {
titleWords: ["桌面", "服务器"], titleWords: ["桌面", "服务器"],
titleSuffix: "上", titleSuffix: "上",
subtitle: "一键创建自进化 Agent", 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", ctaGithub: "GitHub",
installHint: "一行命令安装(内嵌 Node 运行时,解压即用)", /** Line under the CTAs: all installers on /download, CLI / self-hosted below the fold. */
downloadAll: "全部平台下载(macOS / Windows / Linux)",
cliAlt: "命令行与自托管安装 ↓",
stats: [ stats: [
{ value: "1000+", label: "支持模型数量" }, { value: "1000+", label: "支持模型数量" },
{ value: "1×CPU", 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: { install: {
linux: "Linux", linux: "Linux",
macos: "macOS", macos: "macOS",
@@ -85,8 +90,7 @@ export const zh = {
windows: "解压后也可直接双击 install.cmd 完成安装。", windows: "解压后也可直接双击 install.cmd 完成安装。",
}, },
offlineRelease: "前往 GitHub Releases 下载离线安装包", offlineRelease: "前往 GitHub Releases 下载离线安装包",
desktopNote: desktopNote: "已经装了桌面版?CLI 安装与桌面版共用同一份本地数据,两者可以随时并用。",
"更喜欢独立应用?桌面端把 Web 界面与服务端打包成免终端的安装程序(macOS / Windows / Linux),与 CLI 安装共用同一份本地数据。",
desktopPage: "前往桌面端下载页", desktopPage: "前往桌面端下载页",
}, },
@@ -475,7 +479,8 @@ export const zh = {
title: "让复杂的 AI 开发越来越简单", title: "让复杂的 AI 开发越来越简单",
subtitle: subtitle:
"通过不断进化,PenguinHarness 为你提供更高效、更可靠、更低幻觉、更低成本的 Agent 生产力引擎。", "通过不断进化,PenguinHarness 为你提供更高效、更可靠、更低幻觉、更低成本的 Agent 生产力引擎。",
install: "立即安装", download: "下载桌面版",
quickstart: "快速开始",
docs: "阅读文档", docs: "阅读文档",
}, },
+2 -10
View File
@@ -23,22 +23,14 @@ import {
OSS_ORIGIN, OSS_ORIGIN,
RELEASES_URL, RELEASES_URL,
} from "../lib/links"; } from "../lib/links";
import { detectPlatform } from "../lib/platform";
import type { Platform } from "../lib/platform";
import { Section } from "../components/section"; import { Section } from "../components/section";
import { CodeCard } from "../components/code-card"; import { CodeCard } from "../components/code-card";
import { ChevronDownIcon, DownloadIcon, ExternalLinkIcon } from "../components/icons"; import { ChevronDownIcon, DownloadIcon, ExternalLinkIcon } from "../components/icons";
type Platform = "mac" | "windows" | "linux";
const PLATFORMS: 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 { interface Mirror {
tag: string; tag: string;
base: 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 { Link } from "react-router";
import { S } from "../lib/strings"; import { S } from "../lib/strings";
import { DOCS_URL } from "../lib/links"; import { DOCS_URL } from "../lib/links";
import { Section } from "../components/section"; import { Section } from "../components/section";
import { ArrowRightIcon } from "../components/icons"; import { DownloadIcon } from "../components/icons";
export function Cta() { 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 ( return (
<Section> <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"> <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> </p>
<div className="mt-7 flex flex-wrap items-center justify-center gap-3"> <div className="mt-7 flex flex-wrap items-center justify-center gap-3">
<Link <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" 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} <DownloadIcon className="h-4 w-4" />
<ArrowRightIcon className="h-4 w-4" /> {S.cta.download}
</Link> </Link>
<a <Link to="/#quickstart" className={secondary}>
href={DOCS_URL} {S.cta.quickstart}
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>
> <a href={DOCS_URL} className={secondary}>
{S.cta.docs} {S.cta.docs}
</a> </a>
</div> </div>
+34 -59
View File
@@ -1,66 +1,20 @@
/** /**
* Hero: enlarged logo + product name, the one-line headline whose rotating word * Hero: enlarged logo + product name, the one-line headline whose rotating word
* crossfades through a gaussian blur (Desktop <-> Server, localized per dictionary), * 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. * The rotating word is a stacked inline-grid so line width never jumps.
*/ */
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router"; import { Link } from "react-router";
import { S } from "../lib/strings"; import { S } from "../lib/strings";
import { INSTALL_CMD, INSTALL_CMD_WINDOWS, REPO_URL } from "../lib/links"; import { REPO_URL } from "../lib/links";
import { CopyButton } from "../components/copy-button"; import { detectPlatform } from "../lib/platform";
import { ArrowRightIcon, GitHubIcon } from "../components/icons"; import { ArrowRightIcon, DownloadIcon, GitHubIcon } from "../components/icons";
const ROTATE_MS = 2600; 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[] }) { function RotatingWord({ words }: { words: string[] }) {
const [active, setActive] = useState(0); const [active, setActive] = useState(0);
useEffect(() => { useEffect(() => {
@@ -86,6 +40,15 @@ function RotatingWord({ words }: { words: string[] }) {
} }
export function Hero() { 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 ( return (
<section className="relative overflow-hidden"> <section className="relative overflow-hidden">
<div className="hero-dots pointer-events-none absolute inset-0" aria-hidden="true" /> <div className="hero-dots pointer-events-none absolute inset-0" aria-hidden="true" />
@@ -125,10 +88,17 @@ export function Hero() {
style={{ animationDelay: "200ms" }} style={{ animationDelay: "200ms" }}
> >
<Link <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" 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" /> <ArrowRightIcon className="h-4 w-4" />
</Link> </Link>
<a <a
@@ -142,13 +112,18 @@ export function Hero() {
</a> </a>
</div> </div>
<div <p
className="anim-rise mx-auto mt-10 w-fit max-w-full" className="anim-rise mt-5 text-sm text-gray-600 dark:text-gray-400"
style={{ animationDelay: "240ms" }} style={{ animationDelay: "240ms" }}
> >
<InstallBox /> <Link to="/download" className={textLink}>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">{S.hero.installHint}</p> {S.hero.downloadAll}
</div> </Link>
<span className="mx-2 text-gray-300 dark:text-gray-700">·</span>
<Link to="/#quickstart" className={textLink}>
{S.hero.cliAlt}
</Link>
</p>
<dl <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" 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> </p>
</> </>
)} )}
{/* Below both install methods: the desktop app replaces this whole flow for {/* Below both install methods: the desktop app is the headline install path
users who never want a terminal, so it stays visible on every switch. */} (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"> <p className="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
{S.install.desktopNote}{" "} {S.install.desktopNote}{" "}
<Link <Link