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

229 lines
8.0 KiB
TypeScript

/**
* /models/<lab> — every model one lab answers with, on gui.
*
* NO TAILWIND. The trail, the card and the modality chip are
* `components/models/kit`'s, shared with /models and with the model page
* beneath this one; the two sections under the header are
* `components/marketing/band`'s `Band`; the type and the controls are
* @hanzo/ui/marketing's — `Display` for the page heading, `Title`/`Lede` for a
* section's, `Line` for a quiet line and `Action` for a control. No element
* carries a style object.
*
* Three things the page used to decide for itself and now takes from the kit:
*
* - THE HAIRLINE. `border-b border-border` under the header and
* `border-t border-border` above the closing band were two rules for what
* `Band` already draws, in a grey the rest of the site does not use. One
* line per boundary, `--neutral-900`, stated by the frame.
* - THE GUTTER. A flat `px-4` becomes the kit's 16 → 24 → 32.
* - THE CONTROLS. Four links drew a 8px-cornered tile by hand, in the two
* weights `Action` already has. They are `Action` now — the pill.
*/
import type { Metadata } from 'next'
import { KEYS } from '@/components/home/nav-data'
import { ProviderMark } from '@hanzo/app/ProviderMark'
import { Crumbs, Modality, Tile } from '@/components/models/kit'
import { GuestKey } from '@/components/models/guest'
import {
getOrgAndSlug,
canonicalOrg,
orgDisplayName,
formatContext,
modelPagePath,
context,
} from '@/lib/models'
import { fetchModels } from '@/lib/models/served'
import { Box, XStack, YStack } from '@/components/ui'
import { Action, Display, Lede, Line, More, Title } from '@hanzo/ui/marketing'
import { Grid } from '@hanzo/ui/grid'
import { Band } from '@/components/marketing/band'
import { phrase } from '@hanzo/i18n'
import { T } from '@hanzo/i18n/react'
export const revalidate = 3600
interface Props {
params: Promise<{ org: string }>
}
export async function generateStaticParams() {
const data = await fetchModels()
const orgs = new Set<string>()
for (const model of data.models) {
const { org } = getOrgAndSlug(model.id)
orgs.add(canonicalOrg(org))
}
return Array.from(orgs).map((org) => ({ org }))
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { org } = await params
const data = await fetchModels()
const models = data.models.filter((m) => {
const { org: mOrg } = getOrgAndSlug(m.id)
return canonicalOrg(mOrg) === org
})
const providerName = orgDisplayName(org)
const count = models.length
return {
title: `${providerName} Models via Hanzo AI — ${count} models`,
description: `Access ${count} ${providerName} models via Hanzo's OpenAI-compatible API. Single API key, unified billing, and global low-latency routing.`,
openGraph: {
title: `${providerName} Models via Hanzo AI`,
description: `${count} ${providerName} models via one OpenAI-compatible API.`,
url: `https://hanzo.ai/models/${org}`,
siteName: 'Hanzo AI',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: `${providerName} Models via Hanzo AI`,
description: `${count} ${providerName} models. One API key.`,
},
}
}
/** The ground the closing band lifts to, where it is told from its neighbour by
* colour as well as by a rule. */
const LIFT = 'color-mix(in oklab, var(--secondary) 10%, transparent)'
export default async function OrgPage({ params }: Props) {
const { org } = await params
const data = await fetchModels()
const orgModels = data.models.filter((m) => {
const { org: mOrg } = getOrgAndSlug(m.id)
return canonicalOrg(mOrg) === org
})
const providerName = orgDisplayName(org)
if (orgModels.length === 0) {
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>Provider not found</T>
</Title>
<More href="/models" self="center">
<T>{'← Back to all models'}</T>
</More>
</YStack>
</YStack>
)
}
return (
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
{/* Header. Its own shape rather than a Band: the rhythm above it and the
rhythm below it are different numbers, which is the one thing a Band
cannot say. The rule that used to close it is the grid band's now —
same hairline, one element down. */}
<Box
render="section"
pt="$12"
pb="$8"
px="$4"
$sm={{ px: '$5' }}
$lg={{ px: '$6' }}
>
<Box width="100%" maxW={1024} mx="auto">
<Crumbs trail={[{ label: phrase('Models'), href: '/models' }, { label: providerName }]} />
<XStack items="center" gap="$4" mb="$5">
<ProviderMark org={org} size={48} />
<YStack shrink={1}>
<Display>
<T values={{ name: providerName }}>{'{name} Models via Hanzo AI'}</T>
</Display>
<Line render="p" display="block" mt={4} color="var(--muted-foreground)">
<T values={{ n: orgModels.length }}>{'{n} models available'}</T>
</Line>
</YStack>
</XStack>
<Line render="p" display="block" width="100%" maxW={672} color="var(--muted-foreground)">
{orgModels.length === 1 ? (
<T values={{ n: orgModels.length, name: providerName }}>
{"Access all {n} {name} model through Hanzo's OpenAI-compatible API. One key, one bill, no rate limit juggling."}
</T>
) : (
<T values={{ n: orgModels.length, name: providerName }}>
{"Access all {n} {name} models through Hanzo's OpenAI-compatible API. One key, one bill, no rate limit juggling."}
</T>
)}
</Line>
<XStack mt="$5" flexWrap="wrap" gap="$3">
<Action href="/signup" fill>
<T>Get API Access</T>
</Action>
<Action href="https://docs.hanzo.ai">
<T>API Docs</T>
</Action>
</XStack>
<Box mt="$6" maxW={640}>
<GuestKey model="zen-free" />
</Box>
</Box>
</Box>
{/* Model grid */}
<Band pad="$10" measure={1024}>
{/* Stated as the width a card may not go under rather than as two
breakpoints, which is the same picture with nothing to keep in
agreement. 16px is `$4`. */}
<Grid columns={{ min: 280, max: 3 }} gap={16}>
{orgModels.map((model) => {
const ctx = context(model)
return (
<Tile
key={model.id}
href={modelPagePath(model.id)}
name={model.name}
ctx={ctx ? formatContext(ctx) : undefined}
blurb={model.description}
>
{/* The row sits at the foot of the card, however short the blurb. */}
<XStack flexWrap="wrap" gap="$1" mt="auto">
{(model.modalities ?? []).slice(0, 3).map((m) => (
<Modality key={m} name={m} />
))}
</XStack>
</Tile>
)
})}
</Grid>
</Band>
{/* CTA */}
<Band pad="$10" measure={672} ground={LIFT}>
<YStack $platform-web={{ textAlign: 'center' }}>
<Title quiet mb={12}>
<T values={{ name: providerName }}>{'Use {name} models via Hanzo'}</T>
</Title>
<Lede mb={24} mt="$3.5">
<T>One key. One bill. Works with every SDK you already have.</T>
</Lede>
<XStack flexWrap="wrap" justify="center" gap="$3">
<Action href={KEYS} fill>
<T>Start Free</T>
</Action>
<Action href="/models">
<T>Browse All Models</T>
</Action>
</XStack>
</YStack>
</Band>
</Box>
)
}