Skip to content
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ For detailed subsystem docs, see [docs/index.md](./docs/index.md).

> **The website itself is bilingual too — every indexable page must ship a Simplified Chinese sibling under `/zh`.** See [Chinese Website Pages](#chinese-website-pages-zh--mandatory-for-all-indexable-surfaces) below; a new page, tab, or blog post without its `/zh` version is 🔴 BLOCKING on PR review.

> **Cursor Bugbot re-reviews on EVERY push** — each new commit to a PR can surface new inline comments, including on code an earlier review passed. Before merging, loop until convergence: wait for checks (the Bugbot review is one of the PR checks) → fetch unresolved review comments → fix or answer each with a reply → push → repeat until a push produces no new findings. Branch rules require all review threads resolved before merge, so resolve addressed threads as you go.

## Project Overview

InferenceX App — Next.js 16 dashboard for ML inference benchmark data. DB-backed with Neon PostgreSQL, React Query for data fetching, D3.js for charts.
Expand Down Expand Up @@ -130,12 +132,13 @@ The site ships a hand-authored Simplified Chinese sibling for every indexable pa
**Every new indexable page, dashboard tab, or blog post MUST ship its Chinese version in the same PR:**

1. **New page** → create `packages/app/src/app/zh/<route>/page.tsx` with fully translated content and metadata. Metadata: `alternates: zhAlternates('<en-path>')` plus `openGraph.locale: ZH_OG_LOCALE`. Switch the English page's `alternates` to `enAlternates('<en-path>')` so both sides carry bidirectional hreflang. Register the route in `ZH_MIRRORED_ROUTES` (`src/lib/i18n.ts`) so the header nav and EN↔中文 toggle link to it, and add it to the sitemap via `localizedPair()` in `src/app/sitemap.ts`.
2. **New dashboard tab** → add the tab to `ZH_TAB_KEYS`, `TAB_META_ZH`, `TAB_INTRO_ZH`, and `TAB_LABELS_ZH` in `src/lib/tab-meta-zh.ts`, then create `src/app/zh/(dashboard)/<tab>/page.tsx` mirroring the English page with `tabMetadataZh('<tab>')` and a `<ZhTabIntro tab="<tab>" />` block above the chart (the interactive chart UI itself stays English). `tab-meta-zh.test.ts` enforces dictionary completeness.
2. **New dashboard tab** → add the tab to `ZH_TAB_KEYS`, `TAB_META_ZH`, `TAB_INTRO_ZH`, and `TAB_LABELS_ZH` in `src/lib/tab-meta-zh.ts`, then create `src/app/zh/(dashboard)/<tab>/page.tsx` mirroring the English page with `tabMetadataZh('<tab>')` and a `<ZhTabIntro tab="<tab>" />` block above the chart; the chart's own UI strings must follow rule 5. `tab-meta-zh.test.ts` enforces dictionary completeness.
3. **New blog post** → the translation `packages/app/content/blog/zh/<same-filename>.mdx` is REQUIRED in the same PR. Translate frontmatter `title`/`subtitle` and the body; keep `date`, `publishDate`, `modifiedDate`, `tags`, and the filename/slug identical (English and Chinese posts pair by filename; visibility gating always follows the English post's `publishDate`). Rewrite internal `/blog/<slug>` links to `/zh/blog/<slug>`; never alter numbers, code blocks, or `<Figure>`/`<JsonLd>` structure. The `/zh/blog` listing, hreflang, and sitemap pick the file up automatically.
4. **Editing an existing English page or post** → update its Chinese sibling in the same PR. Content drift between languages is a 🔴 BLOCKING review issue.
5. **Shared UI chrome** (headers, footers, dashboard card titles/descriptions, control labels, buttons, nudges) is localized in place, not duplicated: client components call `useLocale()` (`src/lib/use-locale.ts`) and read from a component-local `STRINGS = { en, zh }` dict; server components take an optional `locale` prop passed from the /zh page. The `en` dict must keep the exact original strings so English pages stay byte-identical. New user-visible chrome strings MUST ship both variants. Chart-internal rendering (D3 axes/tooltips/legend series, CSV export) and data-registry display values (model/GPU/framework/precision names) stay English.
6. **Compare slug narrative sync**: the per-slug compare pages are mirrored at `/zh/compare/[slug]` and `/zh/compare-per-dollar/[slug]`; their Chinese prose templates live in `src/lib/compare-ssr-zh.ts`, a 1:1 port of the English templates in `compare-ssr.ts`. Any PR that changes the English narrative templates MUST update the zh port in the same commit.
7. **Intentionally not mirrored** (skip these, or add them to `ZH_MIRRORED_ROUTES` when you do mirror them): `/datasets`, feature-gated tabs (`ai-chart`, `current-inferencex-image`, `feedback`), `feed.xml`/`llms.txt`, and per-post OG images (Chinese posts reuse the English post's OG image — the OG renderer's font has no CJK glyphs).
5. **ALL user-visible UI strings MUST have a Chinese equivalent** — no carve-outs for "chart internals" or "option labels". This includes: headers/footers, card titles/descriptions, control and filter labels, buttons, toggles (Log Scale, Optimal Only, …), nudges, dropdown OPTION display names (Y-axis metric names, token types, scale modes), searchable-select placeholders ("Search…"), table column headers and action buttons ("Prompts"), modal/drawer chrome, legend footnotes, and empty/loading/error messages. Mechanism: client components call `useLocale()` (`src/lib/use-locale.ts`) and read from a component-local `STRINGS = { en, zh }` dict; server components take an optional `locale` prop passed from the /zh page; registry-defined display names (e.g. `Y_AXIS_METRICS`, legend toggle configs) carry a `labelZh` field resolved through a locale-aware label helper at render time. The `en` values must keep the exact original strings so English pages stay byte-identical.
6. **What stays English** (only these): brand/product names, hardware SKUs, model/framework/precision names, units (tok/s/user, GB/s, $/M tok), code identifiers and flags — per the translation quality bar — plus DB-stored _content_ (benchmark rows, dataset conversation text, run logs), which is data, not UI.
7. **Compare slug narrative sync**: the per-slug compare pages are mirrored at `/zh/compare/[slug]` and `/zh/compare-per-dollar/[slug]`; their Chinese prose templates live in `src/lib/compare-ssr-zh.ts`, a 1:1 port of the English templates in `compare-ssr.ts`. Any PR that changes the English narrative templates MUST update the zh port in the same commit.
8. **Every route gets a /zh sibling — including hidden/feature-gated ones** (`/datasets`, `/ai-chart`, `/current-inferencex-image`, `/feedback`, agentic detail pages). Noindex routes keep their noindex on both sides. The only exceptions: `feed.xml`/`llms.txt` (single-language machine feeds) and per-post OG images (Chinese posts reuse the English post's OG image — the OG renderer's font has no CJK glyphs).

## Chart Interpolation — TS and Python Helpers MUST Stay in Sync

Expand Down
2 changes: 1 addition & 1 deletion docs/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ Why the Simplified Chinese site is a hand-authored `/zh` page tree instead of an
- **Reading time is CJK-aware**: `getReadingTime` counts Han characters at 400 chars/min alongside Latin words at 265 wpm; pure word-splitting counts an entire Chinese paragraph as ~1 "word".
- **zh OG images reuse the English post meta** — the `next/og` default Satori font has no CJK glyphs, so a Chinese title would render as tofu. Loading a subset CJK font is a known follow-up.
- **`/zh/inference` canonicalizes to `/zh`**, mirroring the English quirk where `/inference` canonicalizes to `/`.
- **Shared chrome is localized in place** via `useLocale()` + component-local `STRINGS = { en, zh }` dicts (footer, TabNav, dashboard display headings/labels, nudges, preset cards). The `en` dict keeps the exact original strings so English pages are byte-identical; chart-internal rendering and data-registry display values stay English.
- **All UI strings are localized in place** via `useLocale()` + component-local `STRINGS = { en, zh }` dicts (footer, TabNav, dashboard display headings/labels, nudges, preset cards, legend toggles, select placeholders), and registry-defined display names (Y-axis metrics, toggle configs) carry `labelZh` fields resolved through locale-aware helpers. The `en` values keep the exact original strings so English pages are byte-identical. Only brand/product/hardware/framework/precision names, units, code identifiers, and DB-stored content stay English.
- **Compare slug pages are mirrored** at `/zh/compare/[slug]` and `/zh/compare-per-dollar/[slug]`. The Chinese narrative templates live in `compare-ssr-zh.ts` as a 1:1 port of `compare-ssr.ts` (data logic is imported, only sentence templates differ) — the two files must change together.
- **Sitemap pairs**: `localizedPair()` in `sitemap.ts` emits the EN and zh URL together, both carrying the same `alternates.languages` map. Blog posts without a translation fall back to an English-only entry, so a missing translation degrades gracefully instead of 404-ing crawlers.
6 changes: 5 additions & 1 deletion packages/app/src/app/datasets/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';

import { DatasetDetail } from '@/components/datasets/dataset-detail';
import { languageAlternates } from '@/lib/i18n';
import { SITE_URL } from '@semianalysisai/inferencex-constants';

interface Props {
Expand All @@ -14,7 +15,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
return {
title,
description,
alternates: { canonical: `${SITE_URL}/datasets/${slug}` },
alternates: {
canonical: `${SITE_URL}/datasets/${slug}`,
languages: languageAlternates(`/datasets/${slug}`),
},
openGraph: { title: `${title} | InferenceX`, description, url: `${SITE_URL}/datasets/${slug}` },
twitter: { title: `${title} | InferenceX`, description },
};
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/app/datasets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Metadata } from 'next';
import { Card } from '@/components/ui/card';
import { JsonLd } from '@/components/json-ld';
import { DatasetList } from '@/components/datasets/dataset-list';
import { enAlternates } from '@/lib/i18n';
import { SITE_URL } from '@semianalysisai/inferencex-constants';

const DESCRIPTION =
Expand All @@ -11,7 +12,7 @@ const DESCRIPTION =
export const metadata: Metadata = {
title: 'Agentic Datasets',
description: DESCRIPTION,
alternates: { canonical: `${SITE_URL}/datasets` },
alternates: enAlternates('/datasets'),
openGraph: {
title: 'Agentic Datasets | InferenceX',
description: DESCRIPTION,
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
changeFrequency: 'daily',
priority: 0.8,
}),
...localizedPair('/datasets', { lastModified: now, changeFrequency: 'weekly', priority: 0.6 }),
...localizedPair('/blog', { lastModified: now, changeFrequency: 'weekly', priority: 0.8 }),
...getAllPosts().flatMap((post) => {
const entry = {
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/app/zh/(dashboard)/ai-chart/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Metadata } from 'next';

import AiChartDisplay from '@/components/ai-chart/AiChartDisplay';
import { ZhTabIntro } from '@/components/zh/zh-tab-intro';
import { tabMetadataZh } from '@/lib/tab-meta-zh';

export const metadata: Metadata = tabMetadataZh('ai-chart');

export default function ZhAiChartPage() {
return (
<>
<ZhTabIntro tab="ai-chart" />
<AiChartDisplay />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Metadata } from 'next';

import { CurrentImageContent } from '@/components/latest-image/latest-image-content';
import { ZhTabIntro } from '@/components/zh/zh-tab-intro';
import { tabMetadataZh } from '@/lib/tab-meta-zh';

export const metadata: Metadata = tabMetadataZh('current-inferencex-image');

export default function ZhCurrentInferenceXImagePage() {
return (
<>
<ZhTabIntro tab="current-inferencex-image" />
<CurrentImageContent />
</>
);
}
19 changes: 19 additions & 0 deletions packages/app/src/app/zh/(dashboard)/feedback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';

import FeedbackViewer from '@/components/feedback-viewer/FeedbackViewer';
import { ZhTabIntro } from '@/components/zh/zh-tab-intro';
import { tabMetadataZh } from '@/lib/tab-meta-zh';

export const metadata: Metadata = {
...tabMetadataZh('feedback'),
robots: { index: false, follow: false },
};

export default function ZhFeedbackPage() {
return (
<>
<ZhTabIntro tab="feedback" />
<FeedbackViewer />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

import { AgenticPointDetail } from '@/components/inference/agentic-point/agentic-point-detail';
import { isPersistedBenchmarkId } from '@/lib/benchmark-id';

export const metadata: Metadata = {
title: 'Agentic 追踪详情 | InferenceX',
robots: { index: false },
};

export default async function ZhAgenticPointDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const numericId = Number(id);
if (!isPersistedBenchmarkId(numericId)) notFound();
return <AgenticPointDetail id={numericId} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Suspense } from 'react';
import type { Metadata } from 'next';

import { ConversationView } from '@/components/datasets/conversation-view';
import { SITE_URL } from '@semianalysisai/inferencex-constants';

interface Props {
params: Promise<{ slug: string; convId: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug, convId } = await params;
const short = convId.slice(0, 12);
const title = `对话 ${short} | ${slug}`;
const description = `${slug} agentic trace 数据集中对话 ${short} 的逐轮 token 火焰图(缓存前缀 vs 未缓存 input vs output)。`;
return {
title,
description,
alternates: {
canonical: `${SITE_URL}/zh/datasets/${slug}/conversations/${encodeURIComponent(convId)}`,
},
robots: { index: false },
};
}

export default async function ConversationPageZh({ params }: Props) {
const { slug, convId } = await params;
return (
<main className="relative">
<div className="container mx-auto px-4 pb-8 lg:px-8">
<Suspense>
<ConversationView slug={slug} convId={convId} />
</Suspense>
</div>
</main>
);
}
38 changes: 38 additions & 0 deletions packages/app/src/app/zh/datasets/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Metadata } from 'next';

import { DatasetDetail } from '@/components/datasets/dataset-detail';
import { zhAlternates, ZH_OG_LOCALE } from '@/lib/i18n';
import { SITE_URL } from '@semianalysisai/inferencex-constants';

interface Props {
params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const title = `${slug} | Agentic 数据集`;
const description = `${slug} agentic trace 数据集的分布、token 统计及逐对话火焰图。`;
return {
title,
description,
alternates: zhAlternates(`/datasets/${slug}`),
openGraph: {
title: `${title} | InferenceX`,
description,
url: `${SITE_URL}/zh/datasets/${slug}`,
locale: ZH_OG_LOCALE,
},
twitter: { title: `${title} | InferenceX`, description },
};
}

export default async function DatasetDetailPageZh({ params }: Props) {
const { slug } = await params;
return (
<main className="relative">
<div className="container mx-auto px-4 pb-8 lg:px-8">
<DatasetDetail slug={slug} />
</div>
</main>
);
}
93 changes: 93 additions & 0 deletions packages/app/src/app/zh/datasets/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Metadata } from 'next';

import { Card } from '@/components/ui/card';
import { JsonLd } from '@/components/json-ld';
import { DatasetList } from '@/components/datasets/dataset-list';
import { zhAlternates, ZH_OG_LOCALE, ZH_LANG_TAG } from '@/lib/i18n';
import { SITE_URL } from '@semianalysisai/inferencex-constants';

const DESCRIPTION =
'InferenceX agentic 基准测试所回放的真实 Claude Code 对话 trace——方法论、分布及逐对话火焰图。';

export const metadata: Metadata = {
title: 'Agentic 数据集',
description: DESCRIPTION,
alternates: zhAlternates('/datasets'),
openGraph: {
title: 'Agentic 数据集 | InferenceX',
description: DESCRIPTION,
url: `${SITE_URL}/zh/datasets`,
locale: ZH_OG_LOCALE,
},
twitter: { title: 'Agentic 数据集 | InferenceX', description: DESCRIPTION },
};

const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'InferenceX Agentic 数据集',
description: DESCRIPTION,
url: `${SITE_URL}/zh/datasets`,
inLanguage: ZH_LANG_TAG,
};

export default function DatasetsPageZh() {
return (
<main className="relative">
<JsonLd data={jsonLd} />
<div className="container mx-auto flex flex-col gap-6 px-4 pb-8 lg:px-8">
<section>
<Card>
<h1 className="mb-2 text-xl font-semibold text-foreground">Agentic 基准测试数据集</h1>
<p className="mb-3 text-sm text-muted-foreground">
InferenceX 的 agentic 基准测试并非回放合成 prompt——而是回放真实的 Claude Code
编码会话,以<strong>对话 trace</strong>
的形式捕获。每条 trace 是一次完整的多轮会话:包括主 agent 的各轮对话及其调用的所有
subagent,附带每轮的 input/output token 数以及重建 prefix-cache 复用所需的 64-token
KV-cache block hash。这些 trace 在 HuggingFace 上以{' '}
<code>semianalysisai/cc-traces-weka-*</code> 公开发布(apache-2.0 协议)。
</p>

<h2 className="mb-1.5 mt-4 text-sm font-semibold text-foreground">Trace 的采集方式</h2>
<p className="mb-3 text-sm text-muted-foreground">
生产环境中的 Claude Code 会话通过日志代理录制,该代理捕获每个 API 请求的 input 和
output token 数、使用的模型、时间指标(TTFT、token 间延迟),以及一组{' '}
<code>hash_ids</code>(每个对应请求 input 的一个 64-token KV block)。Subagent
调用被归组到其父轮次下。不存储任何 prompt 或 completion 文本——仅保存 token 计数和
block hash,因此语料库可共享,同时仍然是忠实的工作负载回放。
</p>

<h2 className="mb-1.5 mt-4 text-sm font-semibold text-foreground">
缓存前缀与未缓存后缀
</h2>
<p className="mb-3 text-sm text-muted-foreground">
Agentic 工作负载以 prefix 复用为主:每轮都会重新发送不断增长的对话,因此大部分 input
已在前几轮的 KV cache 中。我们精确重建了这一过程。在理想化的无限 cache
下按顺序遍历对话,某一轮的<strong>缓存前缀</strong>是其 <code>hash_ids</code>{' '}
中已出现过的最长前导序列;其余部分是需要(重新)计算的<strong>未缓存后缀</strong>
。每个 block 为 64 个 token;拆分时会限制使缓存 + 未缓存等于该轮的有效
input,即使最后一个 block 不完整。Subagent 在 spawn 时针对父 cache
的快照运行(其上下文独立,不会合并回父级)。
</p>

<h2 className="mb-1.5 mt-4 text-sm font-semibold text-foreground">数据集变体</h2>
<ul className="mb-1 list-disc space-y-1 pl-5 text-sm text-muted-foreground">
<li>
<strong>full</strong> — 所有捕获的请求,不做修改。
</li>
<li>
<strong>256k</strong> — 丢弃 input + output 超过 256,000 token 的请求,确保每轮都在
256k 上下文窗口内(用于在配置 256k 最大上下文的引擎上进行基准测试)。
</li>
</ul>
</Card>
</section>

<section className="flex flex-col gap-3">
<h2 className="text-lg font-semibold text-foreground">数据集</h2>
<DatasetList />
</section>
</div>
</main>
);
}
Loading
Loading