Files
zeekayandGitHub 3df66c3990 i18n: /m* through /r* and the product kits draw their words through t (#70)
Pages from /machines to /risk, the model pages, and the kits they are made of
(page-kit, ProductLanding, LayerPage, Stack, the models kit, band's Head) mark
authored copy where it is written and draw it in the reader's language. A server
page marks a literal phrase('…') or wraps it in <T>; a client page calls t. The
kits translate a string prop through useSay (components/marketing/say), so a
page passes phrase('…') and markup passes as written. An arrow that points
turns over right to left (data-i18n-mirror).

Rebased on main: the fold-based heroes (Fold, PageHero, MachinesHero, the
product and layer pages) keep main's structure and copy, with their label,
title and lede marked phrase. /platform is main's CloudLanding; the deleted
/platform/layers and the old referral components stay deleted.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
2026-10-30 00:18:44 -07:00

501 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* /models/<lab>/<model> — one model, as a card.
*
* Read top to bottom: the hero in the family pages' frame (components/family/
* Hero: a chip, the name, one line, two actions), the links row (the model's
* public repositories and this site's boards for it, lib/models/links), the
* facts as a grid of short labelled cells, the call in four languages
* (lib/models/examples, by the endpoint the model answers at), the price in
* one line, and for Kai its limits. No paragraph restates a cell.
*
* A Kai page sets what ships today beside the Kai-1.7 target, each in its own
* column under its own label, so no target reads as measured. The rate is
* read when the page opens (components/models/Cost); the export states none.
*/
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
import { DOCS, KEYS } from '@/components/home/nav-data'
import { Hero } from '@/components/family/Hero'
import { Target } from '@/components/kai/figures'
import { Crumbs, Tile, DIM, SM } from '@/components/models/kit'
import { Cost } from '@/components/models/Cost'
import { Links } from '@/components/models/Links'
import { Samples } from '@/components/models/Samples'
import {
canonicalOrg,
context,
getOrgAndSlug,
orgDisplayName,
formatContext,
modelPagePath,
type ModelData,
} from '@/lib/models'
import { examples } from '@/lib/models/examples'
import { links } from '@/lib/models/links'
import { fetchModels } from '@/lib/models/served'
import { Box, XStack, YStack } from '@/components/ui'
import { Action, Lede, Line, More, Title } from '@hanzo/ui/marketing'
import { Grid } from '@hanzo/ui/grid'
import { Band } from '@/components/marketing/band'
import { Code } from '@/components/code'
import { Answer } from '@/components/preview/answer'
import { ProviderMark } from '@hanzo/app/ProviderMark'
import { phrase } from '@hanzo/i18n'
import { T } from '@hanzo/i18n/react'
export const revalidate = 3600
interface Props {
params: Promise<{ org: string; model: string }>
}
export async function generateStaticParams() {
const data = await fetchModels()
return data.models
.map((m) => {
const { org, slug } = getOrgAndSlug(m.id)
return { org: canonicalOrg(org), model: slug }
})
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// Next hands a segment over percent-encoded: `claude-opus-4.8:batch` arrives as
// `claude-opus-4.8%3Abatch`, and compared raw it matched no model, so every
// `:batch` page rendered "Model not found".
const { org, model: segment } = await params
const modelSlug = decodeURIComponent(segment)
const data = await fetchModels()
const model = data.models.find((m) => {
const { org: mOrg, slug: mSlug } = getOrgAndSlug(m.id)
return (canonicalOrg(mOrg) === org || mOrg === org) && mSlug === modelSlug
})
if (!model) {
return { title: 'Model Not Found — Hanzo AI' }
}
const desc = summary(model)
return {
title: `${model.name} — Use via Hanzo AI API`,
description: desc,
openGraph: {
title: `${model.name} — Hanzo AI`,
description: desc,
url: `https://hanzo.ai${modelPagePath(model.id)}`,
siteName: 'Hanzo AI',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: `${model.name} — Hanzo AI`,
description: desc,
},
}
}
/**
* The one sentence this page says about itself.
*
* It was computed inside generateMetadata and nowhere else, so the JSON-LD had
* to reach for `model.description` — a field that is often absent, because the
* catalog only writes one where it has something to say. A SoftwareApplication
* with no description is what a search engine got.
*
* The catalog's own words win when there are any; otherwise the model is
* described by what a reader came here to learn — that it is callable through
* one API with one key.
*/
function summary(model: ModelData): string {
const ctx = context(model)
// A catalog description is markdown, and a link clipped mid-URL is a broken
// link in the meta tag, the JSON-LD and llms.txt: the words, not the links.
const d = model.description?.replace(/\[([^\]]*)\]\(<?[^)>]*>?\)/g, '$1').trim()
if (d) {
const maxLen = 75
const clipped = d.length > maxLen ? `${d.slice(0, maxLen).trimEnd()}...` : d
return `${clipped} ${call(model)}${ctx ? ` Context: ${formatContext(ctx)}.` : ''}`
}
return `Use ${model.name} through one Hanzo API key. ${call(model)}`
}
/** A model id that names its free variant: `google/gemma-4-31b-it:free`, `zen-free`. */
const FREE = /[:-]free$/
function buildJsonLd(model: ModelData) {
const ctx = context(model)
return {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: model.name,
description: summary(model),
url: `https://hanzo.ai${modelPagePath(model.id)}`,
applicationCategory: 'AIApplication',
provider: {
'@type': 'Organization',
name: 'Hanzo AI',
url: 'https://hanzo.ai',
},
// A price only where the build knows it: a free variant costs nothing. Every
// other rate is read from GET /v1/pricing when the page opens, so the
// structured data states none rather than a $0 the bill would contradict.
...(FREE.test(model.id)
? { offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD', description: 'Free through the Hanzo AI API' } }
: {}),
...(ctx ? { featureList: [`${formatContext(ctx)} context window`] } : {}),
}
}
/** Where the page says a model is called. */
const call = (m: ModelData) => (m.endpoint === '/v1/decisions' ? 'Call it at POST /v1/decisions.' : "Call via Hanzo's OpenAI-compatible API.")
/** The ground the related band lifts to. */
const LIFT = 'color-mix(in oklab, var(--secondary) 10%, transparent)'
/** A section's heading on this page, one rung under the hero's. */
const COLUMN = {
render: 'h2',
display: 'block',
fontSize: '$4',
lineHeight: 'calc(1.75 / 1.125)',
fontWeight: '600',
color: 'var(--text-primary)',
} as const
/** One labelled fact. `mono` sets an id or a call in the code face. The label is
* authored English, marked `phrase` and drawn through `<T>`; a value that is
* this page's own words is a `<T>` where it is written, and a value read from
* the catalogue is drawn as it is. */
interface Fact {
label: string
value: ReactNode
mono?: boolean
}
/**
* Facts as a grid of cells on a hairline: the label small and quiet, the value
* in ink. Two across from 480px, so a value has room for a sentence's worth.
*/
function Facts({ title, tag, rows }: { title: string; tag?: ReactNode; rows: readonly Fact[] }) {
return (
<YStack gap="$3" data-facts={title} minW={0}>
<XStack items="center" gap="$2" flexWrap="wrap">
<Line {...COLUMN}>
<T>{title}</T>
</Line>
{tag}
</XStack>
<Grid columns={{ min: 200, max: 2 }} gap={1} bg="var(--border)" borderWidth={1} borderColor="var(--border)" rounded="var(--radius-xl)" overflow="hidden">
{rows.map((r, i) => (
// An odd last cell takes the row, so no empty cell shows the hairline's ground.
<YStack key={r.label} gap={4} px="$4" py="$3.5" bg="$background" {...(i === rows.length - 1 && rows.length % 2 ? { '$platform-web': { gridColumn: '1 / -1' } } : {})}>
<Line fontSize="$1" lineHeight="$1" fontWeight="500" color={DIM} $platform-web={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>
<T>{r.label}</T>
</Line>
<Line {...SM} fontWeight="600" color="$ink" {...(r.mono ? { fontFamily: '$mono' } : {})} $platform-web={{ overflowWrap: 'anywhere' }}>
{r.value}
</Line>
</YStack>
))}
</Grid>
</YStack>
)
}
/**
* What each Kai id answers from, and its pin (universe
* charts/app/values/hanzo/{kai,kai-1-4,kai-1-5}.yaml). `hanzo/kai` is `kai`.
*/
const CHECKPOINT: Record<string, string> = {
kai: 'kai-1.3 (pin @551f1990…)',
'kai-1.4': 'kai-1.4 (pin @4a5fdb8c…)',
'kai-1.5': 'kai-1.5 (pin @b8827bb0…)',
}
/** What ships today on a Kai id. The latency is production's, measured on the default id. */
function shipping(model: ModelData): Fact[] {
const id = model.id.replace(/^hanzo\//, '')
return [
{ label: phrase('Model ID'), value: model.id, mono: true },
{ label: phrase('Checkpoint'), value: CHECKPOINT[id] ?? id, mono: true },
{ label: phrase('Parameters'), value: <T>{'~850M (multimodal decision architecture)'}</T> },
...(id === 'kai' ? [{ label: phrase('Latency (GPU)'), value: <T>{'<1 ms p50 (one question, CUDA bf16)'}</T> }] : []),
{ label: phrase('Context reach'), value: <T>{'32,768 tokens (YaRN; trained: 1,024)'}</T> },
{ label: phrase('Modality'), value: <T>Text and structured state</T> },
{ label: phrase('Output tokens'), value: <T>{'0 (typed probability distribution)'}</T> },
{ label: phrase('Endpoint'), value: 'POST /v1/decisions', mono: true },
{ label: phrase('Architecture'), value: <T>{'Proprietary Decision Transformer + factorized cross-attention'}</T> },
{ label: phrase('Safety qualification'), value: <T>{'Not yet qualified: a sealed evaluation is being authored'}</T> },
{ label: phrase('Price'), value: <Cost id={model.id} lab={false} /> },
]
}
/** The Kai-1.7 multimodal design's figures: targets, not measurements. */
const TARGETS: readonly Fact[] = [
{ label: phrase('Parameters'), value: <T>{'~850M (text, vision, audio, video)'}</T> },
{ label: phrase('Resident memory'), value: <T>{'Under 2.0 GiB'}</T> },
{ label: phrase('Text latency'), value: <T>{'<1 ms p50 (GPU), multi-pass adaptive refinement'}</T> },
{ label: phrase('Image latency'), value: <T>{'360–400 ms (image + text state)'}</T> },
{ label: phrase('Encoders'), value: <T>{'Vision · Audio · Video'}</T> },
{ label: phrase('Reasoning'), value: 'reasoning: "auto" | "fast" | "deep"', mono: true },
]
/** What Kai does not do today. */
const LIMITS: readonly string[] = [
phrase('Reads text and structured state. Images, audio and video are the Kai-1.7 target.'),
phrase('Reaches 32,768 tokens by position; accuracy at that length is qualified for kai-1.4 and kai-1.5 only.'),
phrase('At most 100 questions a request. More questions, or a state past 32,768 tokens, is refused with 422.'),
phrase('The reasoning field is the Kai-1.7 target: the API refuses it today (400).'),
]
/** Any other model's facts, from its catalogue record. */
function specs(model: ModelData): Fact[] {
const ctx = context(model)
const out: Fact[] = []
if (ctx) out.push({ label: phrase('Context Window'), value: formatContext(ctx) })
if (model.spec?.params) out.push({ label: phrase('Parameters'), value: model.spec.params })
if (model.spec?.activeParams) out.push({ label: phrase('Active Params'), value: model.spec.activeParams })
if (model.spec?.arch) out.push({ label: phrase('Architecture'), value: model.spec.arch })
if (model.modalities?.length) out.push({ label: phrase('Modalities'), value: model.modalities.join(', ') })
out.push({ label: phrase('Price'), value: <Cost id={model.id} lab={model.category === 'third-party'} /> })
out.push({ label: phrase('Model ID'), value: model.id, mono: true })
return out
}
/** The hero's one line: Kai's own, else the catalogue's first sentence (the
* catalogue's words, drawn as they are). */
function line(model: ModelData, kai: boolean): ReactNode {
if (kai) return <T>Decisions, not completions.</T>
if (model.endpoint === '/v1/decisions') return <T>Typed decisions at POST /v1/decisions.</T>
const first = model.description?.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? model.description
return first && first.length <= 160 ? first : <T values={{ name: model.name }}>{'{name}, through one API key.'}</T>
}
export default async function ModelPage({ params }: Props) {
// Next hands a segment over percent-encoded: `claude-opus-4.8:batch` arrives as
// `claude-opus-4.8%3Abatch`, and compared raw it matched no model, so every
// `:batch` page rendered "Model not found".
const { org, model: segment } = await params
const modelSlug = decodeURIComponent(segment)
const data = await fetchModels()
const model = data.models.find((m) => {
const { org: mOrg, slug: mSlug } = getOrgAndSlug(m.id)
return (canonicalOrg(mOrg) === org || mOrg === org) && mSlug === modelSlug
})
if (!model) {
return (
<YStack
minH="100vh"
items="center"
justify="center"
bg="$background"
$platform-web={{ color: 'var(--foreground)', textAlign: 'center' }}
>
<YStack>
<Title render="h1" quiet mb={8}>
<T>Model not found</T>
</Title>
<More href="/models" self="center">
<T>{'← Back to all models'}</T>
</More>
</YStack>
</YStack>
)
}
// THE LAB'S PAGE IS AT ITS CANONICAL NAME, and this page's own segment is not
// it: `[org]/page.tsx` generates its params from `canonicalOrg(org)`, so the
// raw segment (/models/~anthropic, /models/meta-llama) names a route the
// export never writes. The trail and "View all" link the canonical one.
const lab = canonicalOrg(org)
const providerName = orgDisplayName(lab)
const kai = model.family === 'kai'
const decides = model.endpoint === '/v1/decisions'
const { ts, py, curl, go } = examples(model)
const jsonLd = buildJsonLd(model)
// Related models: same org, excluding current. A Zen model's are Zen: the
// bare ids filed under the same org are mostly other labs' short names.
const zen = model.id.startsWith('zen')
const related = data.models
.filter((m) => {
const { org: mOrg } = getOrgAndSlug(m.id)
return (canonicalOrg(mOrg) === lab || mOrg === org) && m.id !== model.id && (!zen || m.id.startsWith('zen'))
})
.slice(0, 6)
const primary = decides ? { label: phrase('Get decisions'), href: '#quickstart' } : { label: phrase('Get API key'), href: KEYS }
const action = kai
? { label: phrase('Explore Kai'), href: '/kai' }
: decides
? { label: phrase('Read the docs'), href: `${DOCS}/docs/decisions` }
: { label: phrase('Try in Chat'), href: '/chat' }
return (
<>
{/* JSON-LD structured data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
<Hero
trail={
<Crumbs
trail={[
{ label: phrase('Models'), href: '/models' },
{ label: providerName, href: `/models/${lab}` },
{ label: modelSlug },
]}
/>
}
chip={model.tier ? `${providerName} · ${model.tier}` : providerName}
title={model.name}
lede={line(model, kai)}
primary={primary}
action={action}
below={<Links links={links(model)} center />}
figure={
!decides && model.description ? (
<Answer
models={[
{
id: model.id,
name: model.name,
says: model.description,
mark: <ProviderMark org={lab} model={model.id} size={16} />,
facts: [...(context(model) ? [formatContext(context(model)!)] : []), ...(model.modalities ?? []).slice(0, 2)],
},
]}
/>
) : undefined
}
/>
<Band id="specs" pad="$10" measure={1024}>
{kai ? (
<Grid columns={{ min: 320, max: 2 }} gap={24}>
<Facts title={phrase('Shipping today')} rows={shipping(model)} />
<Facts title={phrase('In development')} tag={<Target />} rows={TARGETS} />
</Grid>
) : (
<YStack gap="$5">
<Facts title={phrase('Specifications')} rows={specs(model)} />
{model.features && model.features.length > 0 ? (
<Line {...SM} color={DIM}>
{model.features.join(' · ')}
</Line>
) : null}
</YStack>
)}
</Band>
<Band id="quickstart" pad="$10" measure={896}>
<YStack gap="$4">
<XStack items="baseline" justify="space-between" gap="$3" flexWrap="wrap">
<Line {...COLUMN}>
<T>Quick start</T>
</Line>
<More href={KEYS}>
<T>{'Get an API key →'}</T>
</More>
</XStack>
<Samples
samples={[
{ lang: 'TypeScript', code: ts, sample: <Code lang="typescript" code={ts} /> },
{ lang: 'Python', code: py, sample: <Code lang="python" code={py} /> },
{ lang: 'cURL', code: curl, sample: <Code lang="bash" code={curl} /> },
{ lang: 'Go', code: go, sample: <Code lang="go" code={go} /> },
]}
/>
<Line {...SM} color={DIM} data-price="">
{kai ? (
<T values={{ cost: <Cost id={model.id} lab={model.category === 'third-party'} /> }}>
{'{cost}. A native decision writes 0 output tokens; a decision Jev or a language model answers bills at that model’s rates, output included.'}
</T>
) : (
<Cost id={model.id} lab={model.category === 'third-party'} />
)}
</Line>
</YStack>
</Band>
{kai ? (
<Band id="limits" pad="$10" measure={896}>
<YStack gap="$3">
<Line {...COLUMN}>
<T>Limits</T>
</Line>
<YStack render="ul" gap="$2">
{LIMITS.map((l) => (
<XStack render="li" key={l} items="baseline" gap="$3">
<Box render="span" width={4} height={4} mt={8} shrink={0} rounded="var(--radius-full)" bg="var(--muted-foreground)" />
<Line {...SM} color={DIM}>
<T>{l}</T>
</Line>
</XStack>
))}
</YStack>
</YStack>
</Band>
) : null}
{/* Related models */}
{related.length > 0 && (
<Band pad="$10" measure={1024} ground={LIFT}>
<Line {...COLUMN} mb={24}>
<T values={{ name: providerName }}>{'More from {name}'}</T>
</Line>
<Grid columns={{ min: 240, max: 3 }} gap={12}>
{related.map((m) => {
const mCtx = context(m)
return (
<Tile
key={m.id}
href={modelPagePath(m.id)}
name={m.name}
ctx={mCtx ? formatContext(mCtx) : undefined}
blurb={m.description}
/>
)
})}
</Grid>
<Box mt="$5">
<More href={`/models/${lab}`}>
<T values={{ name: providerName }}>{'View all {name} models →'}</T>
</More>
</Box>
</Band>
)}
{/* CTA */}
<Band pad="$10" measure={672}>
<YStack $platform-web={{ textAlign: 'center' }}>
<Title quiet mb={12}>
<T values={{ name: model.name }}>{'Use {name} via Hanzo AI'}</T>
</Title>
<Lede mb={24} mt="$3.5">
{decides ? (
<T>One API key. One call to POST /v1/decisions.</T>
) : (
<T>One API key. Every major model. OpenAI-compatible. Start free.</T>
)}
</Lede>
<XStack flexWrap="wrap" justify="center" gap="$3">
<Action href={KEYS} fill>
<T>Get an API key</T>
</Action>
<Action href="/models">
<T>Browse All Models</T>
</Action>
</XStack>
</YStack>
</Band>
</Box>
</>
)
}