mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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
229 lines
8.0 KiB
TypeScript
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>
|
|
)
|
|
}
|