Files
zeekay e981114824 chat: a signed-out reader asks the buying agent, and it routes them to pay
/chat signed out is the buying agent (components/sales/Guest, Agent): the
question the home composer carried in ?q= is asked of cloud's
POST /v1/ai/sales, the reply is drawn as text, and each next step the
server built is a control: a call (the lead is filed first, then the
calendar opens on it), the sign-up, a plan's card checkout, Add funds. A
quiet line says it is a limited preview and how many messages are left; at
the ceiling the sign-up takes the composer's place. A conversation's own
address still signs in, and a signed-in reader keeps the app.

/buying-specialist is the same agent; its keyword script, its typed prices
and its claims to see an account are gone. The model pages' no-key card is
the real lane, POST /v1/chat/public, not a guest key that answers 401.

Measured in the site's names: chat_started, cta_clicked {cta: kind, plan,
agent: sales}, sales_contacted. The floating privacy button is hidden as
room.css meant: the rule names its tag to outrank gui's atomic class.
2026-10-27 01:29:38 -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 />
</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>
)
}