docs(readme,landing): lead the story with automatic agent building (#52)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-24 00:57:28 +08:00
1 parent 33e5f0bcd1
commit 6bbdac132f
15 files changed
+229 -47

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+5 -3
View File
@@ -37,6 +37,8 @@ export function Nav() {
const sectionLabel: Record<(typeof SECTION_IDS)[number], string> = {
highlights: S.nav.highlights,
quickstart: S.nav.quickstart,
cases: S.nav.cases,
scenarios: S.nav.scenarios,
benchmark: S.nav.benchmark,
contract: S.nav.contract,
features: S.nav.features,
@@ -152,7 +154,7 @@ export function Nav() {
</Link>
<nav
className="relative ml-4 hidden items-center gap-0.5 md:flex"
className="relative ml-4 hidden items-center gap-0.5 xl:flex"
aria-label="Primary"
onMouseLeave={hidePill}
>
@@ -184,7 +186,7 @@ export function Nav() {
onClick={() => setOpen((v) => !v)}
aria-label={open ? S.nav.closeMenu : S.nav.openMenu}
aria-expanded={open}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 md:hidden dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900"
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 xl:hidden dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900"
>
{open ? <XIcon className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
</button>
@@ -193,7 +195,7 @@ export function Nav() {
{open && (
<nav
className="anim-fade border-t border-gray-200 bg-white px-4 py-2 md:hidden dark:border-gray-800 dark:bg-gray-950"
className="anim-fade border-t border-gray-200 bg-white px-4 py-2 xl:hidden dark:border-gray-800 dark:bg-gray-950"
aria-label="Mobile"
>
<div className="flex flex-col py-1">{mobileLinks}</div>
+9 -2
View File
@@ -13,7 +13,8 @@ export function Section({
id?: string;
eyebrow?: string;
title?: string;
subtitle?: string;
/** A list renders one line per entry (deliberate breaks, e.g. the 1×/100× pair); a string wraps naturally. */
subtitle?: string | readonly string[];
children: ReactNode;
className?: string;
}) {
@@ -35,7 +36,13 @@ export function Section({
)}
{subtitle && (
<p className="mt-4 text-base leading-7 text-pretty text-gray-600 dark:text-gray-400">
{subtitle}
{typeof subtitle === "string"
? subtitle
: subtitle.map((line) => (
<span key={line} className="block">
{line}
</span>
))}
</p>
)}
</div>
+2
View File
@@ -5,6 +5,8 @@
export const SECTION_IDS = [
"highlights",
"quickstart",
"cases",
"scenarios",
"benchmark",
"contract",
"features",
+31 -7
View File
@@ -20,6 +20,8 @@ export const en: Strings = {
nav: {
highlights: "Highlights",
quickstart: "Quick start",
cases: "Cases",
scenarios: "Scenarios",
benchmark: "Benchmark",
contract: "CONTRACT.md",
features: "Features",
@@ -45,11 +47,13 @@ export const en: Strings = {
},
hero: {
badge: "Build AI agents, with an Agent",
titlePrefix: "Efficient Self-Improving Harness for ",
titleWords: ["Developers", "Enterprises"],
badge: "Agents building agents",
// The trailing space lives in the PREFIX (a breakable space before the nowrap
// span) — inside titleNoWrap it would glue "Builder" to the unbreakable chunk.
titlePrefix: "Your Automated Agent Builder ",
titleNoWrap: "Lives on Your ",
titleWords: ["Desktop", "Server"],
titleSuffix: "",
titleSuffixNoWrap: "",
keywords: ["Lightweight", "Efficient", "Open Source"],
ctaPrimary: "Get started",
ctaGithub: "GitHub",
@@ -99,8 +103,10 @@ export const en: Strings = {
compare: {
eyebrow: "vs. LangChain",
title: "The gap between 1× and 100×",
subtitle:
"With LangChain, you build agents by hand — at 1× speed. With PenguinHarness, agents build agents — at 100×.",
subtitle: [
"With LangChain, you build agents by hand — at 1× speed.",
"With PenguinHarness, agents build agents — at 100×.",
],
langchain: {
name: "LangChain",
speed: "1×",
@@ -212,6 +218,24 @@ export const en: Strings = {
],
},
scenarios: {
eyebrow: "Scenarios",
title: "From screening centers to factory floors",
subtitle: "Real deployments in production — not concept demos.",
items: [
{
title: "Screening-report QC",
alt: "A CT scanner room at a health screening center",
body: "A health-screening group handed report QC to an agent on a locally deployed Qwen3 14B — data never leaves the facility. A review round that took 30 minutes by hand now clears 30 reports a minute, with findings in line with medical experts and several times the review capacity.",
},
{
title: "Production-line inspection",
alt: "An automated semiconductor production line",
body: "A manufacturer runs inspection agents across its production lines, watching equipment around the clock and trying automated recovery first when something goes wrong. Downtime is down 65%; output roughly doubled.",
},
],
},
contract: {
eyebrow: "A contract for stable evolution",
title: "CONTRACT.md",
@@ -266,7 +290,7 @@ export const en: Strings = {
benchmark: {
eyebrow: "Benchmark",
title: "Each with its flagship model — comparable quality, 1-2 orders of magnitude cheaper",
title: "Outstanding results at tens of times less cost",
subtitle:
"Every product runs the model it is normally paired with, head-to-head against Claude Code and OpenAI Codex on two suites: comparable accuracy, tens of times the difference in spend.",
higherBetter: "higher is better",
+33 -10
View File
@@ -21,6 +21,8 @@ export const zh = {
nav: {
highlights: "特色",
quickstart: "快速开始",
cases: "案例",
scenarios: "应用场景",
benchmark: "评测",
contract: "CONTRACT.md",
features: "功能",
@@ -48,14 +50,15 @@ export const zh = {
hero: {
badge: "让 Agent 为你构建 Agent",
/**
* Rotating headline: {titlePrefix}{word}{titleSuffix}{titleSuffixNoWrap} — the
* word cycles through titleWords with a gaussian-blur crossfade; titleSuffixNoWrap
* renders as an unbreakable span so CJK line-breaking lands on the phrase boundary.
* Headline: {titlePrefix}<nowrap>{titleNoWrap}{word}{titleSuffix}</nowrap>, the word
* rotating through titleWords (桌面/服务器 — where the builder lives; the README
* states Desktop only). The nowrap span pins the wrap point in front of it, so a
* break never splits "Agent Builder" / "Lives on Your Desktop" mid-phrase.
*/
titlePrefix: "专为",
titleWords: ["开发者", "企业"],
titleSuffix: "设计的",
titleSuffixNoWrap: "高效自进化 Harness",
titlePrefix: "全自动 Agent 构建器,",
titleNoWrap: "住在你的",
titleWords: ["桌面", "服务器"],
titleSuffix: "上",
keywords: ["轻量", "高效", "开源"],
ctaPrimary: "快速开始",
ctaGithub: "GitHub",
@@ -103,8 +106,10 @@ export const zh = {
compare: {
eyebrow: "对比 LangChain",
title: "1× 与 100× 的差距",
subtitle:
"使用 LangChain,以 1 倍速度人工构建 Agent;使用 PenguinHarness,以 100 倍速度用 Agent 构建 Agent。",
subtitle: [
"使用 LangChain,以 1 倍速度人工构建 Agent;",
"使用 PenguinHarness,以 100 倍速度用 Agent 构建 Agent。",
],
langchain: {
name: "LangChain",
speed: "1×",
@@ -208,6 +213,24 @@ export const zh = {
],
},
scenarios: {
eyebrow: "应用场景",
title: "从体检中心到工厂车间",
subtitle: "都是已经上线的真实部署,不是概念演示。",
items: [
{
title: "体检报告质控",
alt: "体检中心的 CT 检查室",
body: "一家体检机构把报告质控交给了 Agent,跑在本地部署的 Qwen3 14B 上,数据不出机房。过去一轮人工复核要 30 分钟,现在一分钟自动过 30 份,核查结论与医学专家基本一致,审核产能提升数倍。",
},
{
title: "产线设备巡检",
alt: "自动化半导体生产线",
body: "一家制造企业在各条流水线上部署巡检 Agent,全天候盯着设备状态,异常时先尝试自动恢复。停机时间减少 65%,产出提升约 2 倍。",
},
],
},
contract: {
eyebrow: "稳定进化的契约",
title: "CONTRACT.md",
@@ -260,7 +283,7 @@ export const zh = {
benchmark: {
eyebrow: "Benchmark",
title: "各用各的旗舰模型,效果同级,成本低一到两个数量级",
title: "以几十分之一的成本,跑出优异的效果",
subtitle:
"每个产品搭配它常用的模型,与 Claude Code、OpenAI Codex 在两套题库上正面对比:准确率同级,花的钱差出几十倍。",
higherBetter: "越高越好",
+2
View File
@@ -5,6 +5,7 @@ import { Compare } from "../sections/compare";
import { SelfImprove } from "../sections/self-improve";
import { Quickstart } from "../sections/quickstart";
import { Cases } from "../sections/cases";
import { Scenarios } from "../sections/scenarios";
import { Benchmark } from "../sections/benchmark";
import { Contract } from "../sections/contract";
import { Features } from "../sections/features";
@@ -22,6 +23,7 @@ export function HomePage() {
<SelfImprove />
<Quickstart />
<Cases />
<Scenarios />
<Benchmark />
<Contract />
<Features />
+13 -10
View File
@@ -1,8 +1,8 @@
/**
* Hero: enlarged logo + product name, the slogan inside the pill badge, then a
* bilingual headline whose rotating word crossfades through a gaussian blur
* (Developers <-> Enterprises), three keywords, the install one-liner and stats.
* The rotating word is a stacked inline-grid so line width never jumps.
* Hero: enlarged logo + product name, the slogan inside the pill badge, then the
* one-line headline whose rotating word crossfades through a gaussian blur
* (Desktop <-> Server / 桌面 <-> 服务器), three keywords, the install one-liner
* and stats. The rotating word is a stacked inline-grid so line width never jumps.
*/
import { Fragment, useEffect, useState } from "react";
import { Link } from "react-router";
@@ -57,16 +57,19 @@ export function Hero() {
<span className="h-1.5 w-1.5 rounded-full bg-brand-500" aria-hidden="true" />
{S.hero.badge}
</p>
{/* No text-balance: balance may break inside the breakable prefix ("…Agent /
Builder…"); greedy wrapping + the nowrap span pins the desktop break to
"Your Automated Agent Builder / Lives on Your Desktop". */}
<h1
className="anim-rise mx-auto mt-6 max-w-full text-3xl font-semibold tracking-tight text-balance sm:text-5xl"
className="anim-rise mx-auto mt-6 max-w-full text-3xl font-semibold tracking-tight sm:text-5xl"
style={{ animationDelay: "80ms" }}
>
{S.hero.titlePrefix}
<RotatingWord words={S.hero.titleWords} />
{S.hero.titleSuffix}
{S.hero.titleSuffixNoWrap && (
<span className="whitespace-nowrap">{S.hero.titleSuffixNoWrap}</span>
)}
<span className="whitespace-nowrap">
{S.hero.titleNoWrap}
<RotatingWord words={S.hero.titleWords} />
{S.hero.titleSuffix}
</span>
</h1>
<div
+105
View File
@@ -0,0 +1,105 @@
/**
* Deployment scenarios as an auto-cycling overlapped pair: the front card sits
* left at 92% width, the other slides right so its strip peeks past the front
* card's right edge, tucked underneath — on swap the two cross horizontally
* (both live in one grid cell, so the container takes the tallest card's height
* and nothing jumps). The front card holds for ROTATE_MS (long enough to actually
* read the paragraph); hovering the deck pauses the timer — nobody loses their
* place mid-read; clicking the peeking card (or a dot) switches manually. The
* covered card is a pointer-only shortcut and stays aria-hidden — dots carry the
* accessible switching — and prefers-reduced-motion disables auto-advance.
* SHOTS is index-aligned with S.scenarios.items; the photos are theme-agnostic,
* so a single asset serves light and dark (unlike the Cases screenshots).
*/
import { useEffect, useState } from "react";
import { S } from "../lib/strings";
import { Section } from "../components/section";
import medicalShot from "../assets/case-medical-qc.webp";
import lineShot from "../assets/case-line-inspection.webp";
const SHOTS = [medicalShot, lineShot];
const ROTATE_MS = 12000;
export function Scenarios() {
const items = S.scenarios.items;
const [active, setActive] = useState(0);
const [paused, setPaused] = useState(false);
useEffect(() => {
if (paused || items.length < 2) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const timer = setInterval(() => setActive((i) => (i + 1) % items.length), ROTATE_MS);
return () => clearInterval(timer);
}, [paused, items.length]);
return (
<Section
id="scenarios"
eyebrow={S.scenarios.eyebrow}
title={S.scenarios.title}
subtitle={S.scenarios.subtitle}
>
<div
className="mx-auto max-w-5xl"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<div className="grid">
{items.map((c, i) => {
const front = i === active;
return (
<figure
key={c.title}
aria-hidden={!front}
// The peeking card is itself the switch target (its visible strip is
// small, so the whole card surface accepts the click).
onClick={front ? undefined : () => setActive(i)}
// Cards are 92% wide in a shared cell: the front one holds the left
// edge; the covered one shifts its own 8.7% (= the container's 8%)
// so exactly that strip peeks past the front card's right edge.
className={`col-start-1 row-start-1 w-[92%] overflow-hidden rounded-2xl border bg-white transition-all duration-500 dark:bg-gray-900 ${
front
? "z-10 translate-x-0 border-gray-200 opacity-100 dark:border-gray-800"
: "z-0 translate-x-[8.7%] scale-[0.96] cursor-pointer border-gray-200/70 opacity-60 dark:border-gray-800/70"
}`}
>
<div className="md:flex md:min-h-[340px] md:items-stretch">
<img
src={SHOTS[i]}
alt={c.alt}
loading="lazy"
className="aspect-[16/9] w-full object-cover md:aspect-auto md:w-[48%]"
/>
<figcaption className="p-7 md:flex md:flex-1 md:flex-col md:justify-center md:p-12">
<h3 className="text-xl font-semibold sm:text-2xl">{c.title}</h3>
<p className="mt-4 text-[15px] leading-7 text-gray-600 dark:text-gray-400">
{c.body}
</p>
</figcaption>
</div>
</figure>
);
})}
</div>
{/* Manual switch doubles as the position indicator */}
<div className="mt-6 flex justify-center gap-2">
{items.map((c, i) => (
<button
key={c.title}
type="button"
aria-label={c.title}
aria-current={i === active}
onClick={() => setActive(i)}
className={`h-1.5 rounded-full transition-all duration-300 ${
i === active
? "w-6 bg-gray-800 dark:bg-gray-200"
: "w-3 bg-gray-300 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-500"
}`}
/>
))}
</div>
</div>
</Section>
);
}