Files
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

214 lines
7.4 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'
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}>Provider not found</Title>
<More href="/models" self="center">
← Back to all models
</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: 'Models', href: '/models' }, { label: providerName }]} />
<XStack items="center" gap="$4" mb="$5">
<ProviderMark org={org} size={48} />
<YStack shrink={1}>
<Display>{providerName} Models via Hanzo AI</Display>
<Line render="p" display="block" mt={4} color="var(--muted-foreground)">
{orgModels.length} models available
</Line>
</YStack>
</XStack>
<Line render="p" display="block" width="100%" maxW={672} color="var(--muted-foreground)">
Access all {orgModels.length} {providerName} model{orgModels.length !== 1 ? 's' : ''} through
Hanzo's OpenAI-compatible API. One key, one bill, no rate limit juggling.
</Line>
<XStack mt="$5" flexWrap="wrap" gap="$3">
<Action href="/signup" fill>
Get API Access
</Action>
<Action href="https://docs.hanzo.ai">
API Docs
</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}>Use {providerName} models via Hanzo</Title>
<Lede mb={24} mt="$3.5">
One key. One bill. Works with every SDK you already have.
</Lede>
<XStack flexWrap="wrap" justify="center" gap="$3">
<Action href={KEYS} fill>
Start Free
</Action>
<Action href="/models">
Browse All Models
</Action>
</XStack>
</YStack>
</Band>
</Box>
)
}