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