Files
hanzo.ai/app/(marketing)/zen/models/view.tsx
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

770 lines
28 KiB
TypeScript

'use client'
/**
* /zen/models — the full Zen catalog, on gui.
*
* The four sections are `components/marketing/band`'s `Band` (the rule above,
* the rhythm around, the gutter beside and the measure it centres on); the code
* frame is `components/models/kit`'s `Sample`, shared with /models and the
* model page; the type and the controls are @hanzo/ui's marketing components —
* `Display` for the page heading, `Title`/`Lede` for a section's, `Line` for
* every other run of words, `Chip` for a chip and `Action` for a control. No
* element carries a style object: a table is gui frames stating their table
* display, a motion host is the `render` of the frame it moves.
*
* Six things the page used to decide for itself and now takes from the kit,
* each a VISIBLE change rather than a translation:
*
* - THE HAIRLINE and THE GUTTER. `border-t border-border` at two sections and
* a `px-4 md:px-8 lg:px-12` ladder become `Band`'s single `--neutral-900`
* rule and the site's 16 → 24 → 32.
* - THE PAGE HEADING. `text-3xl sm:4xl lg:5xl xl:6xl` — four rungs, one of
* which no other heading on this site stands on — becomes `display`, which
* is the one display ladder here: 32 / 40 / 52.
* - THE CONTROLS. Six links drew two button shapes by hand. They are `action`
* at its two weights, and the quick-nav chips are `pill`, whose hover is the
* one thing they were each restating.
* - THE WIDE COLUMNS. `hidden md:table-cell` and `hidden lg:table-cell` are
* one question — a column a narrow screen has no room for — asked at two
* breakpoints. `Spare` (components/pricing/kit) already answers it, so Arch
* joins Context at 48rem rather than waiting for 64.
* - THE CELL PADDING. `px-4 md:px-6` on every one of twelve cells becomes a
* flat 16. The table already scrolls inside its own box, so the wider step
* was buying nothing a reader could not already reach.
* - THE WINDOW DOTS. The sample's three dots were drawn in ONE grey here and
* as a red/yellow/green traffic light on the other two pages that draw the
* same frame. `Sample` draws the traffic light.
*
* NO TAILWIND. The last two — a hover and a breakpoint, the two things a style
* attribute cannot carry — are a role and a gui prop this site already had, and
* neither needed a name of its own:
*
* - the ground of a model row on approach is `Cell`. It reads as a grid
* word and is not one: `Cell` adds no geometry, only a ground and its
* change, which is the whole of what a row wants. `Leaf` would have been
* wrong — block, full height, padding and a radius on a `<tr>` — and that
* is what made this look like it needed a role of its own.
* - the description under a model's name asks the same question the
* Context and Arch columns ask, at the same breakpoint, through gui's
* `$max-md`. The text is in the document either way — what the breakpoint
* decides is whether a phone's row is two lines or five.
*/
import { dev } from '@hanzo/app/lib/host'
import { KEYS } from '@/components/home/nav-data'
import { JOBS, LINEUP, NEXT } from '@/lib/zen'
import React, { useState, useEffect, type ReactNode } from "react";
import { motion } from "@/components/motion";
import { Spinner } from "@hanzo/gui";
import zen from "@/lib/data/zen.json";
import type { ZenModel } from "@zenlm/models";
/** The price list, on the gateway of the host this page is open on. */
const pricing = () => `${api()}/v1/pricing`;
import {
ArrowRight,
Code2,
Zap,
Brain,
Terminal,
Eye,
Search,
ChevronDown,
ChevronUp,
Image,
Volume2,
Sparkles,
} from "lucide-react";
import { Box, View, XStack, YStack } from '@hanzo/ui'
import { Grid } from '@hanzo/ui/grid'
import { Band } from '@/components/marketing/band'
import { Sample, DIM, SM, XS } from '@/components/models/kit'
import { CELL, ROW, Spare, TABLE } from '@/components/pricing/kit'
import { mix } from '@hanzo/app/lib/mix'
import { api } from '@hanzo/app/lib/api'
import { Action, Chip, Display, Lede, Line, Title, Cell } from '@hanzo/ui/marketing'
// Derive context window from features string (e.g. "202k context window" -> "202K")
function extractContext(features: string[]): string {
const ctx = features.find((f) => f.toLowerCase().includes("context"));
if (!ctx) return "N/A";
const match = ctx.match(/(\d+[kKmM]+)/);
return match ? match[1].toUpperCase() : ctx;
}
/** A tier, as the three values that tell it from the tier above. */
const TIERS: Record<string, { bg: string; color: string; borderColor: string }> = {
"ultra max": { bg: mix('var(--primary)', 15), color: 'var(--foreground)', borderColor: 'var(--white-30)' },
ultra: { bg: mix('var(--primary)', 10), color: mix('var(--foreground)', 60), borderColor: 'var(--border)' },
"pro max": { bg: mix('var(--primary)', 10), color: mix('var(--foreground)', 70), borderColor: 'var(--border)' },
pro: { bg: mix('var(--primary)', 10), color: mix('var(--foreground)', 70), borderColor: 'var(--border)' },
starter: { bg: 'var(--white-05)', color: 'var(--white-60)', borderColor: 'var(--white-10)' },
};
/** The badge's own shape, which every tier shares. */
const BADGE = {
display: 'inline-flex',
items: 'center',
px: 8,
py: 2,
borderWidth: 1,
borderStyle: 'solid',
rounded: 'var(--radius-full)',
fontSize: 'var(--text-floor)',
lineHeight: '1.6',
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: '0.05em',
} as const
function TierBadge({ tier }: { tier: string }) {
return (
<Line {...BADGE} {...(TIERS[tier] || TIERS.pro)}>
{tier}
</Line>
);
}
const fmt = (val: number | null | undefined) => {
if (val == null) return "N/A";
return `$${val}`;
};
/* ── the values the table is set in ─────────────────────────────────────────── */
/** The row of the flagship, on a ground of the accent; it holds under the pointer. */
const ULTRA = 'color-mix(in oklab, var(--primary) 5%, transparent)'
/** A cell: its room, and the table display a frame has to be told. A cell is
* a `Line`, so it takes the row's type the way a plain cell would. */
const BODY = { render: 'td', display: CELL, p: 16 } as const
/** The words a head cell is set in, and the room around them. */
const CAPS = { ...XS, fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em', color: DIM } as const
const HEAD = { render: 'th', display: CELL, px: 16, py: 12, text: 'left', ...CAPS } as const
/** A fact about the model, at the small rung. */
const INK = mix('var(--foreground)', 80)
const FACT = { ...SM, color: INK } as const
const LOUD = { color: 'var(--foreground)', fontWeight: '500' } as const
/** The label over one third of an opened row. */
const KEY = {
fontSize: 'var(--text-floor)',
lineHeight: '1.6',
textTransform: 'uppercase',
letterSpacing: '0.05em',
color: 'var(--muted-foreground)',
} as const
/** A price, boxed. */
const BOX = { p: 8, rounded: 'var(--radius-md)', borderWidth: 1, borderColor: 'var(--border)', bg: 'var(--secondary)' } as const
// Group models by generation + purpose
interface HanzoModel {
name: string;
fullName: string;
description: string;
features: string[];
tier: string;
specs: { params: string; arch: string };
/** The price list's rates; null when it states none (contact sales), absent when the price list was not read. */
pricing?: { input: number | null; output: number | null; cacheRead: number | null; cacheWrite: number | null; perUnit?: number } | null;
pricingUnit?: string;
endpoint?: string;
/**
* Served via the API only — no public weights. Distinct from "no retail
* price", which is `pricing == null`; `contactSales` used to braid the two
* into one flag derived from a `contact-sales` status @zenlm/models does not
* have, so it was always false and the badge never rendered.
*/
cloudOnly?: boolean;
}
interface ModelGroup {
id: string;
title: string;
description: string;
icon: React.ElementType;
models: HanzoModel[];
pricingMode?: "token" | "unit" | "contact";
}
function groupModels(models: HanzoModel[]): ModelGroup[] {
const zen5 = models.filter((m) => m.name.startsWith("zen5"));
const zen4General = models.filter(
(m) =>
(m.name.startsWith("zen4") || m.name.startsWith("zen4.")) &&
!m.name.includes("coder") &&
!m.name.includes("thinking")
);
const zen4Thinking = models.filter((m) => m.name === "zen4-thinking");
const zen4Coder = models.filter((m) => m.name.startsWith("zen4-coder"));
const zen3Core = models.filter(
(m) =>
m.name.startsWith("zen3") &&
!m.name.includes("image") &&
!m.name.includes("audio") &&
!m.name.includes("asr") &&
!m.name.includes("tts") &&
!m.name.includes("embedding") &&
!m.name.includes("reranker")
);
const zen3Image = models.filter((m) => m.name.startsWith("zen3-image"));
const zen3Audio = models.filter(
(m) =>
m.name.startsWith("zen3-audio") ||
m.name.startsWith("zen3-asr") ||
m.name.startsWith("zen3-tts")
);
const zen3Retrieval = models.filter(
(m) =>
m.name.startsWith("zen3-embedding") ||
m.name.startsWith("zen3-reranker")
);
const groups: ModelGroup[] = [];
if (zen5.length > 0) {
groups.push({
id: "zen5",
title: "Zen5 -- Agentic Frontier",
description: "Agentic frontier models trained on 10B+ tokens of real-world tool use and multi-step reasoning.",
icon: Sparkles,
models: zen5,
pricingMode: "contact",
});
}
if (zen4General.length > 0 || zen4Thinking.length > 0) {
groups.push({
id: "zen4",
title: "Zen4 -- Flagship Language Models",
description: "Flagship and high-capability language models for general reasoning, analysis, and generation.",
icon: Brain,
models: [...zen4General, ...zen4Thinking],
});
}
if (zen4Coder.length > 0) {
groups.push({
id: "zen4-coder",
title: "Zen4 Coder -- Code Generation",
description: "Specialized code models for generation, review, debugging, and full-precision analysis.",
icon: Code2,
models: zen4Coder,
});
}
if (zen3Core.length > 0) {
groups.push({
id: "zen3",
title: "Zen3 -- Multimodal & Specialized",
description: "Multimodal, vision-language, edge, and safety models.",
icon: Eye,
models: zen3Core,
});
}
if (zen3Image.length > 0) {
groups.push({
id: "zen3-image",
title: "Zen3 Image -- Generation",
description: "Text-to-image generation from fast prototyping to broadcast quality.",
icon: Image,
models: zen3Image,
pricingMode: "unit",
});
}
if (zen3Audio.length > 0) {
groups.push({
id: "zen3-audio",
title: "Zen3 Audio -- Speech & Voice",
description: "Speech-to-text, text-to-speech, and real-time streaming ASR.",
icon: Volume2,
models: zen3Audio,
pricingMode: "unit",
});
}
if (zen3Retrieval.length > 0) {
groups.push({
id: "zen3-retrieval",
title: "Zen3 Retrieval -- Embedding & Reranking",
description: "High-quality embeddings and rerankers for RAG, search, and classification.",
icon: Search,
models: zen3Retrieval,
});
}
return groups;
}
const ModelRow = ({ model, pricingMode }: { model: HanzoModel; pricingMode?: string }) => {
const [expanded, setExpanded] = useState(false);
const isUltraMax = model.tier === "ultra max";
const renderPricing = () => {
if (model.pricing === undefined) {
return <Line color={DIM}>—</Line>;
}
if (model.pricing == null) {
return (
<Line fontSize="$1" fontStyle="italic" color={DIM}>
Contact Sales
</Line>
);
}
if (pricingMode === "unit" && model.pricing.perUnit != null) {
return (
<Line {...LOUD}>
${model.pricing.perUnit}
<Line fontSize="$1" color={DIM} fontWeight="400">
/{model.pricingUnit || "unit"}
</Line>
</Line>
);
}
return (
<>
<Line {...LOUD}>{fmt(model.pricing.input)}</Line>
<Line color={DIM}> / </Line>
<Line {...LOUD}>{fmt(model.pricing.output)}</Line>
</>
);
};
return (
<>
{/* `Cell` is the site's surface whose ground answers a pointer. It adds
no geometry at all, so a table row wears it exactly as a grid tile
does, and both grounds are its props. See the file header. */}
<Cell
render="tr"
bg={isUltraMax ? ULTRA : 'transparent'}
borderWidth={0}
borderTopWidth={1}
borderStyle="solid"
borderTopColor="var(--border)"
cursor="pointer"
onClick={() => setExpanded(!expanded)}
hoverStyle={{ bg: isUltraMax ? ULTRA : 'color-mix(in oklab, var(--secondary) 50%, transparent)' }}
>
<Line {...BODY}>
<XStack flexWrap="wrap" items="center" gap="$2">
<Line fontWeight="600" color="var(--foreground)" fontSize="$2">
{model.fullName?.split(" — ")[0] || model.fullName?.split(" -- ")[0] || model.name}
</Line>
<TierBadge tier={model.tier} />
{model.cloudOnly && (
<Line
px={8}
py={2}
rounded="var(--radius-full)"
bg={mix('var(--primary)', 20)}
color="var(--primary)"
fontSize="var(--text-floor)"
lineHeight="1.6"
fontWeight="500"
>
Cloud API
</Line>
)}
</XStack>
{/* The same question the Context and Arch columns ask, at the same
breakpoint, on a `<p>` instead of a `<td>`. */}
<Line render="p" display="block" mt={4} {...XS} color={DIM} $max-md={{ display: 'none' }}>
{model.description}
</Line>
</Line>
<Line {...BODY} {...FACT}>
{model.specs.params}
</Line>
<Spare render="td" p={16}>
<Line display="block" {...FACT}>
{extractContext(model.features)}
</Line>
</Spare>
<Spare render="td" p={16}>
<Line display="block" {...FACT}>
{model.specs.arch}
</Line>
</Spare>
<Line {...BODY} {...FACT} text="right">
{renderPricing()}
</Line>
<Line {...BODY} px={8} color="var(--muted-foreground)">
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</Line>
</Cell>
{expanded && (
<View render="tr" display={ROW} borderTopWidth={1} borderColor={mix('var(--border)', 50)}>
<Line {...BODY} {...{ colSpan: 6 }} bg={mix('var(--background)', 80)} fontSize="$2">
<Grid columns={{ min: 200, max: 3 }} gap={24}>
<YStack>
<Line render="h4" mb={8} {...KEY}>
Architecture
</Line>
<Line render="p" display="block" color={INK}>
<Line {...LOUD}>{model.specs.params}</Line>
</Line>
<Line render="p" display="block" color={DIM}>
{model.specs.arch}
</Line>
{model.endpoint && (
<Line render="p" display="block" mt={4} fontFamily="$mono" fontSize="$1" color={DIM}>
{model.endpoint}
</Line>
)}
</YStack>
<YStack>
<Line render="h4" mb={8} {...KEY}>
Features
</Line>
<YStack gap="$1">
{model.features.map((f, i) => (
<XStack key={i} items="center" gap="$2">
<View render="span" width={6} height={6} rounded="var(--radius-full)" bg="var(--primary)" />
<Line color={INK}>{f}</Line>
</XStack>
))}
</YStack>
</YStack>
<YStack>
<Line render="h4" mb={8} {...KEY}>
Pricing
</Line>
{model.pricing === undefined ? (
<Line render="p" display="block" color={DIM}>
The price list could not be read.
</Line>
) : model.pricing == null ? (
<Line render="p" display="block" fontStyle="italic" color={DIM}>
Contact sales for pricing
</Line>
) : pricingMode === "unit" && model.pricing.perUnit != null ? (
<YStack {...BOX}>
<Line {...KEY}>Per {model.pricingUnit || "unit"}</Line>
<Line {...LOUD}>${model.pricing.perUnit}</Line>
</YStack>
) : (
<Grid columns={2} gap={8}>
<YStack {...BOX}>
<Line {...KEY}>Input</Line>
<Line {...LOUD}>{fmt(model.pricing.input)}</Line>
</YStack>
<YStack {...BOX}>
<Line {...KEY}>Output</Line>
<Line {...LOUD}>{fmt(model.pricing.output)}</Line>
</YStack>
</Grid>
)}
</YStack>
</Grid>
</Line>
</View>
)}
</>
);
};
const ModelGroupSection = ({ group }: { group: ModelGroup }) => {
const Icon = group.icon;
const pricingLabel = group.pricingMode === "unit" ? "Price" : group.pricingMode === "contact" ? "Availability" : "Input / Output";
return (
<Box render="section" id={group.id} mb="$11">
<XStack items="center" gap="$4" mb="$5">
<XStack
shrink={0}
width={48}
height={48}
items="center"
justify="center"
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--border)"
bg="$raised"
$platform-web={{ color: 'var(--foreground)' }}
>
<Icon size={24} />
</XStack>
<YStack shrink={1}>
<Title quiet>{group.title}</Title>
<Line render="p" display="block" {...SM} color={DIM}>
{group.description}
</Line>
</YStack>
</XStack>
<Box overflowX="auto">
<View
render="table"
display={TABLE}
width="100%"
overflow="hidden"
rounded="var(--radius-xl)"
borderWidth={1}
borderColor="var(--border)"
bg="var(--background)"
$platform-web={{ borderCollapse: 'collapse' }}
>
<thead>
<View render="tr" display={ROW} bg={mix('var(--secondary)', 80)}>
<Line {...HEAD}>Model</Line>
<Line {...HEAD}>Params</Line>
<Spare render="th" px={16} py={12}>
<Line display="block" text="left" {...CAPS}>
Context
</Line>
</Spare>
<Spare render="th" px={16} py={12}>
<Line display="block" text="left" {...CAPS}>
Arch
</Line>
</Spare>
<Line {...HEAD} text="right">
{pricingLabel}
</Line>
<Line {...HEAD} width={32} px={8} />
</View>
</thead>
<tbody>
{group.models.map((model) => (
<ModelRow key={model.name} model={model} pricingMode={group.pricingMode} />
))}
</tbody>
</View>
</Box>
</Box>
);
};
// Static fallback data — matches API schema exactly
// Adapter: convert a Zen catalogue row (lib/data/zen.json, scripts/zen.mjs) → HanzoModel (no duplicate data)
function zenToHanzo(m: ZenModel): HanzoModel {
const params = m.spec.params && !['N/A', 'TBA', 'TBD'].includes(m.spec.params)
? m.spec.activeParams ? `${m.spec.params} (${m.spec.activeParams} active)` : m.spec.params
: (m.spec.params || 'TBA');
return {
name: m.id,
fullName: m.fullName,
description: m.description,
features: m.features,
tier: m.tier,
specs: { params, arch: m.spec.arch || '' },
cloudOnly: m.status === 'cloud-only',
endpoint: m.id.includes('embedding') ? '/v1/embeddings'
: m.id.includes('reranker') ? '/v1/rerank'
: m.id.includes('image') ? '/v1/images/generations'
: m.id.includes('audio') || m.id.includes('asr') || m.id.includes('tts') ? '/v1/audio/transcriptions'
: undefined,
};
}
const FALLBACK_MODELS: HanzoModel[] = (zen as ZenModel[]).map(zenToHanzo);
/** One of the five figures under the lede, and what it counts. */
const STAT = { fontSize: '$6', lineHeight: '1.4', fontWeight: '700', color: 'var(--foreground)' } as const
const UNIT = { ...XS, textTransform: 'uppercase', letterSpacing: '0.05em', color: DIM } as const
const RISE = { initial: { opacity: 0, y: 16 }, animate: { opacity: 1, y: 0 } } as const
const ZenModels = ({ quickstart }: { quickstart: ReactNode }) => {
const [hanzoModels, setHanzoModels] = useState<HanzoModel[]>(FALLBACK_MODELS);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(pricing())
.then((res) => res.json())
.then((data) => {
const models = data.hanzoModels || [];
if (models.length > 0) setHanzoModels(models);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
const groups = groupModels(hanzoModels);
const totalModels = hanzoModels.length;
const tokenModels = hanzoModels.filter((m) => m.pricing?.input != null && m.pricing.input > 0);
const minPrice = tokenModels.length > 0 ? Math.min(...tokenModels.map((m) => m.pricing!.input!)) : 0.15;
const maxCtx = "2M";
return (
<Box minH="100vh" $platform-web={{ color: 'var(--foreground)' }}>
<Box render="main">
{/* Hero. Its own shape rather than a Band: the rhythm above it and the
rhythm below it are different numbers, and the light behind it wants
a box of its own to be clipped by. */}
<Box
render="section"
position="relative"
overflow="hidden"
pt="$12"
pb="$10"
px="$4"
$sm={{ px: '$5' }}
$lg={{ px: '$6' }}
>
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1280} mx="auto" $platform-web={{ textAlign: 'center' }}>
<Box render={<motion.div {...RISE} transition={{ duration: 0.4 }} />} mb={24}>
<Chip px={12} py={4} fontSize="$1" lineHeight="calc(1 / 0.75)" fontWeight="500" color="var(--foreground)">
<Zap size={12} />
{LINEUP} available now · {NEXT.name} in research preview
</Chip>
</Box>
<Display
render={<motion.h1 {...RISE} transition={{ duration: 0.4, delay: 0.05 }} />}
mb={24}
>
Zen Models
<br />
Complete API Catalog
</Display>
<Lede
render={<motion.p {...RISE} transition={{ duration: 0.4, delay: 0.1 }} />}
mb={32}
width="100%"
maxW={768}
mx="auto"
>
{JOBS} {totalModels} foundation models from{' '}
{/* `underline` is inline and the hover is DROPPED: @hanzo/design
underlines any link inside a paragraph on approach, so the
utility that used to sit here asked for the opposite of what
shipped. */}
<Line href="https://zoo.industries" target="_blank" rel="noopener noreferrer" textDecorationLine="underline" color="var(--foreground)">
Zoo Labs Foundation
</Line>{' '}
across language, code, vision, image, audio, and retrieval.
Zen MoDE (Mixture of Diverse Experts) architecture. From ${minPrice}/MTok.
</Lede>
{/* Quick stats */}
<XStack render={<motion.div {...RISE} transition={{ duration: 0.4, delay: 0.15 }} />} flexWrap="wrap" justify="center" gap={32} mb={32}>
{[
{ label: "Models", value: String(totalModels) },
{ label: "Max Context", value: maxCtx },
{ label: "From", value: `$${minPrice}/MTok` },
{ label: "Architectures", value: "MoE + Dense" },
{ label: "Modalities", value: "7" },
].map((stat) => (
<YStack key={stat.label}>
<Line {...STAT}>{stat.value}</Line>
<Line {...UNIT}>{stat.label}</Line>
</YStack>
))}
</XStack>
{/* Tier legend */}
<XStack render={<motion.div {...RISE} transition={{ duration: 0.4, delay: 0.2 }} />} flexWrap="wrap" justify="center" gap={12} mb={32}>
{Object.keys(TIERS).map((tier) => (
<TierBadge key={tier} tier={tier} />
))}
</XStack>
{/* CTAs */}
<XStack render={<motion.div {...RISE} transition={{ duration: 0.4, delay: 0.25 }} />} flexWrap="wrap" justify="center" items="center" gap={16}>
<Action href="#zen5" fill>
Explore Models
<ArrowRight size={16} />
</Action>
<Action href="/pricing">
Full Pricing
</Action>
<Action href={KEYS} target="_blank" rel="noopener noreferrer">
<Terminal size={16} />
Get API key
</Action>
</XStack>
</Box>
</Box>
{/* Model Groups */}
<Band pad="$10" measure={1280} rule={false}>
{loading ? (
<XStack items="center" justify="center" py="$12">
<Spinner size="small" color="$soft" mr={8} />
<Line color="var(--muted-foreground)">Loading live pricing...</Line>
</XStack>
) : (
<>
{/* Quick nav */}
<XStack mb="$11" flexWrap="wrap" justify="center" gap="$3">
{groups.map((group) => {
const Icon = group.icon;
return (
<Chip
render="a"
key={group.id}
href={`#${group.id}`}
px={16}
py={8}
color="var(--muted-foreground)"
>
<Icon size={16} />
{group.title.split(" -- ")[0]}
</Chip>
);
})}
</XStack>
{groups.map((group) => (
<ModelGroupSection key={group.id} group={group} />
))}
</>
)}
</Band>
{/* API Usage Example */}
<Band pad="$10" measure={896} ground="color-mix(in oklab, var(--background) 50%, transparent)">
<Box
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} />}
mb={32}
$platform-web={{ textAlign: 'center' }}
>
<Title quiet mb={12}>Quick Start</Title>
<Lede mt="$3.5">
Drop-in OpenAI-compatible API. Switch models with one line.
</Lede>
</Box>
<Sample lang="python">{quickstart}</Sample>
</Band>
{/* CTA Section */}
<Band pad="$11" measure={1280}>
<Box
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} />}
$platform-web={{ textAlign: 'center' }}
>
<Title quiet mb={16}>
Ready to build with Zen?
</Title>
<Lede mb={32} width="100%" maxW={672} mx="auto" mt="$3.5">
All {totalModels} Zen models available via OpenAI-compatible
API. Start building today.
</Lede>
<XStack flexWrap="wrap" justify="center" gap="$4">
<Action href="/signup" fill>
Get Started
<ArrowRight size={16} />
</Action>
<Action href={dev()}>
Try Hanzo Dev
</Action>
<Action href="/pricing">
View Pricing
</Action>
</XStack>
</Box>
</Band>
</Box>
</Box>
);
};
export default ZenModels;