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:
1 parent
e07106a924
commit
8fc26cc44f
7 files changed
+86
-90
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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: "阅读文档",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user