mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
1233 lines
45 KiB
TypeScript
1233 lines
45 KiB
TypeScript
'use client'
|
|
|
|
import React, { useEffect, useRef, useState } from "react"
|
|
import { useReducedMotion } from "framer-motion"
|
|
import { ArrowRight } from "lucide-react"
|
|
import { Github } from "@/components/brand"
|
|
import { CopyButton } from "@hanzo/ui/product"
|
|
import { Surface } from '@/components/product/Surface'
|
|
import Workloads from '@/components/cloud/Workloads'
|
|
import { KEYS } from "@/components/home/nav-data"
|
|
import { ready, useSummary } from '@/lib/api/use'
|
|
import { cloudCategories, getPrimitive, spell, tour } from '@/lib/data/cloud-primitives'
|
|
import { MOVED } from '@/lib/publish'
|
|
import { Box, Caret, Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid, tracks } from '@hanzo/ui/grid'
|
|
import { tap } from '@hanzo/app/lib/tap'
|
|
import { Action, Display, Lede, More, Title } from '@hanzo/ui/marketing'
|
|
|
|
const DOCS = "https://docs.hanzo.ai/docs/quickstart"
|
|
const GH = "https://github.com/hanzoai"
|
|
const STATUS = "https://status.hanzo.ai"
|
|
|
|
/**
|
|
* The image we operate for you, and the one you can pull — same artifact.
|
|
* Verified against GHCR: the tag resolves and the manifest answers.
|
|
*/
|
|
const DEPLOY = 'docker run -p 8080:8080 ghcr.io/hanzoai/cloud'
|
|
|
|
/** The installer, verified live: hanzo.sh answers with a POSIX shell script. */
|
|
const INSTALL = 'curl -fsSL https://hanzo.sh | sh'
|
|
|
|
/**
|
|
* The whole API, from your own machine. This is the CLI's OWN spelling —
|
|
* `Serve` is a registered subcommand in `hanzoai/cli` (src/main.rs) and takes
|
|
* `cloud` for the entire API or one service by name.
|
|
*
|
|
* NOT `hanzo up`, which several pages on this site print and which the shipped
|
|
* CLI does not have: an unrecognised first word is read as a task for the
|
|
* coding agent, so the command does not fail — it does something else entirely.
|
|
*/
|
|
const SERVE = 'hanzo serve cloud'
|
|
|
|
/**
|
|
* cloud.hanzo.ai's landing page — the body of `app/(marketing)/cloud/page.tsx`,
|
|
* promoted to this host's web root by the Dockerfile's `SURFACE=cloud`.
|
|
*
|
|
* It renders the PAGE only. The header and footer come from the shared
|
|
* `(marketing)` layout, which is the same chrome hanzo.ai wears — so the two
|
|
* hosts are one product with one nav, and this file cannot grow a second,
|
|
* divergent one.
|
|
*
|
|
* WHAT THIS PAGE SELLS, and what it does not. The apex sells Hanzo OS and the
|
|
* thesis behind it. This sells the ABSTRACTION: you can put a model, an agent,
|
|
* a database and a production application somewhere without first assembling a
|
|
* hyperscaler out of parts. From code to production without building the cloud
|
|
* first.
|
|
*
|
|
* That is a different argument from the one this page used to run, which was
|
|
* an inventory — the categories, then every product in them, twice. Counting
|
|
* the catalog explains what we own; it never explains why a reader should want
|
|
* one of these instead of ten. So the page now runs the workloads and the
|
|
* planes underneath them, and the catalog is a single explorer at the bottom
|
|
* for the reader who came to browse. `/products` is still the whole index.
|
|
*
|
|
* THE ARC, one claim to a screen: the cloud, the shape of an application on it,
|
|
* compute, where it can run, models, data, network, what you can see, what
|
|
* holds it, the one API under all of it, deploying, taking it with you, paying
|
|
* for it, then the catalog.
|
|
*
|
|
* NOTHING HERE ASSERTS A NUMBER IT CANNOT COUNT. The model count is asked of
|
|
* the gateway at read time. The API paths are the catalog's, which is itself
|
|
* gated on the served OpenAPI document. The tour's operations are checked
|
|
* against that document on every build. A claim this page cannot check is a
|
|
* claim this page does not make.
|
|
*/
|
|
|
|
/* ------------------------------------------------------------------ tour --- */
|
|
|
|
/** The products whose API this beat's operation belongs to, from the catalogue. */
|
|
function productsFor(path: string) {
|
|
return cloudCategories
|
|
.flatMap((c) => c.items)
|
|
.filter((p) => {
|
|
const api = p.api?.replace(/\/$/, '')
|
|
if (!api) return false
|
|
return path === api || path.startsWith(api + '/') || api.startsWith(path.replace(/\/$/, ''))
|
|
})
|
|
}
|
|
|
|
const TYPE_MS = 26
|
|
const HOLD_MS = 2100
|
|
|
|
/**
|
|
* The tour — one story, told as the operations that would actually run it.
|
|
*
|
|
* WHY IT CAN BE TRUSTED. Every beat is resolved at BUILD time against the served
|
|
* `/v1/openapi.json` (see `scripts/sync-catalog.mjs`): a beat survives iff the
|
|
* document carries its path AND its verb, so an invented path and a real path
|
|
* with the wrong verb are both dropped before they can reach a reader. The line
|
|
* is ours; the verb, the path and the description under it are the API's own
|
|
* words. The product chips come from the same catalogue the menu reads — and
|
|
* where an operation has no product row yet, none are drawn rather than a
|
|
* stand-in being found for it.
|
|
*
|
|
* IT IS THE PROOF, NOT THE DECORATION. It sits under the one-API claim because
|
|
* it is what settles it: a dozen operations across six planes, one key, and not
|
|
* one line of integration between them. A reader who does not believe the claim
|
|
* above can read the paths.
|
|
*
|
|
* MOTION IS OPTIONAL, not decorated with an opt-out. Under
|
|
* `prefers-reduced-motion` there is no typing and no auto-advance: the first
|
|
* beat is simply shown, whole, and the dots still work as buttons. Someone who
|
|
* wants to read is never a hostage — the panel pauses on the button, and on
|
|
* hover or keyboard focus, and a dot both selects a beat and stops the clock.
|
|
*
|
|
* NOTHING RESIZES. The line reserves two lines, the description two more and the
|
|
* chip row one, so a short beat and a long one occupy the same box and the fold
|
|
* never moves under the reader. One `setTimeout` chain drives it — no interval
|
|
* per beat, no timer left running when the tab is hidden.
|
|
*/
|
|
function Tour() {
|
|
const reduced = useReducedMotion()
|
|
const beats = tour.beats
|
|
const [at, setAt] = useState(0)
|
|
const [typed, setTyped] = useState(reduced ? Infinity : 0)
|
|
const [held, setHeld] = useState(false)
|
|
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
|
|
const beat = beats[at]
|
|
const line = beat?.line ?? ''
|
|
const paused = held || !!reduced
|
|
|
|
useEffect(() => {
|
|
if (paused || !beats.length) return
|
|
if (typed < line.length) {
|
|
timer.current = setTimeout(() => setTyped((n) => n + 1), TYPE_MS)
|
|
} else {
|
|
timer.current = setTimeout(() => {
|
|
setAt((i) => (i + 1) % beats.length)
|
|
setTyped(0)
|
|
}, HOLD_MS)
|
|
}
|
|
return () => clearTimeout(timer.current)
|
|
}, [typed, line.length, paused, beats.length])
|
|
|
|
// A hidden tab should not be typing to nobody.
|
|
useEffect(() => {
|
|
const onVis = () => setHeld(document.hidden ? true : false)
|
|
document.addEventListener('visibilitychange', onVis)
|
|
return () => document.removeEventListener('visibilitychange', onVis)
|
|
}, [])
|
|
|
|
const pick = (i: number) => {
|
|
setAt(i)
|
|
setTyped(Infinity)
|
|
setHeld(true)
|
|
}
|
|
|
|
if (!beats.length) return null
|
|
const shown = reduced ? line : line.slice(0, typed)
|
|
const lit = productsFor(beat.path)
|
|
|
|
return (
|
|
/* The three hold-handlers ride a plain element: gui's frame types no focus
|
|
or pointer handler, so on a Box they would reach nothing and the panel
|
|
would never pause for a reader. */
|
|
<div
|
|
onMouseEnter={() => setHeld(true)}
|
|
onMouseLeave={() => setHeld(false)}
|
|
onFocusCapture={() => setHeld(true)}
|
|
>
|
|
<Box
|
|
rounded="var(--radius-2xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--white-10)"
|
|
bg="var(--white-03)"
|
|
p={20}
|
|
$sm={{ p: 28 }}
|
|
>
|
|
<Text render="p" display="block" fontSize="$1" lineHeight="$1" textTransform="uppercase" letterSpacing={0.55} color="var(--text-tertiary)">
|
|
{tour.story}
|
|
</Text>
|
|
|
|
{/* Two lines reserved, so a short beat and a long one are the same box. */}
|
|
<Text render="p" display="block" mt={12} minH="3.5rem" fontSize="$4" lineHeight="1.4rem" fontWeight="500" color="$ink">
|
|
{shown}
|
|
{/* The bar trailing the line as it types; the blink is @hanzo/ui's Caret. */}
|
|
{!reduced && typed < line.length ? <Caret render="span" display="inline-flex" ml={2} height={20} width={2} y={2} bg="var(--white-80)" /> : null}
|
|
</Text>
|
|
|
|
{/* The operation that beat would actually run, in the API's own words. */}
|
|
<Box mt="$1" minH="3.25rem">
|
|
<Text render="code" fontFamily="$mono" fontSize="$1" lineHeight="$1" color="var(--text-secondary)">
|
|
<Text color="var(--muted-foreground)">{beat.method}</Text> {beat.path}
|
|
</Text>
|
|
<Text render="p" display="block" mt={4} fontSize="$1" lineHeight="1.1172rem" color="var(--muted-foreground)" numberOfLines={2}>
|
|
{beat.summary}
|
|
</Text>
|
|
</Box>
|
|
|
|
{/* Real products, where the catalogue carries one for that operation. */}
|
|
<XStack mt="$3" minH="1.75rem" flexWrap="wrap" items="center" gap={6}>
|
|
{lit.map((p) => {
|
|
const Icon = p.icon
|
|
return (
|
|
<Text key={p.title} {...PILL} gap={6} px={10}>
|
|
<Text display="inline-flex" color="var(--muted-foreground)">
|
|
<Icon size={14} />
|
|
</Text>{' '}
|
|
{p.title}
|
|
</Text>
|
|
)
|
|
})}
|
|
</XStack>
|
|
|
|
{/* The button sits BESIDE the dots where there is room and ABOVE them
|
|
where there is not: at 390px the twelve targets wrap to three rows and
|
|
a vertically centred button reads as floating in the middle of them. */}
|
|
{/* A `YStack`, never a `Box`: a Box is `display: block`, so a gap and an
|
|
alignment on one reach nothing. The column IS the layout here. */}
|
|
<YStack
|
|
mt="$4"
|
|
items="flex-start"
|
|
gap="$3"
|
|
$sm={{ flexDirection: 'row', items: 'center' }}
|
|
>
|
|
<Text render={<button type="button" onClick={() => setHeld((v) => !v)} aria-pressed={held} />} {...PILL} px={12} bg="transparent" cursor="pointer">
|
|
{paused ? 'Play' : 'Pause'}
|
|
</Text>
|
|
<XStack mx={-4} flexWrap="wrap" items="center">
|
|
{beats.map((b, i) => (
|
|
// The DOT is drawn inside the target rather than being it. This site
|
|
// floors every button at 24px (44px where a finger points), which is
|
|
// WCAG 2.5.8 / 2.5.5 and correct — a 6px control is not clickable by
|
|
// anyone. Sized as a dot it obeyed the floor by stretching into a
|
|
// 6x24 bar; sized as a target with a dot in it, both are right. The
|
|
// width follows the same pointer question the height already asks,
|
|
// so a finger gets 44x44 and a mouse keeps a 24px dot row.
|
|
<View
|
|
key={b.path + b.method}
|
|
render={<button type="button" onClick={() => pick(i)} aria-label={b.line} aria-current={i === at ? 'true' : undefined} />}
|
|
group
|
|
width={24}
|
|
height={24}
|
|
items="center"
|
|
justify="center"
|
|
cursor="pointer"
|
|
$touchable={{ width: 44, height: 44 }}
|
|
>
|
|
<View
|
|
width={6}
|
|
height={6}
|
|
rounded="var(--radius-full)"
|
|
bg={i === at ? '$ink' : 'var(--text-tertiary)'}
|
|
transition="quickest"
|
|
$group-hover={i === at ? undefined : { bg: 'var(--muted-foreground)' }}
|
|
/>
|
|
</View>
|
|
))}
|
|
</XStack>
|
|
</YStack>
|
|
</Box>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** A word in an outline: the tour's products and its play control. */
|
|
const PILL = {
|
|
display: 'inline-flex',
|
|
items: 'center',
|
|
gap: '$2',
|
|
py: 4,
|
|
borderWidth: 1,
|
|
borderColor: '$edge',
|
|
rounded: 'var(--radius-full)',
|
|
bg: 'var(--glass)',
|
|
fontSize: '$1',
|
|
lineHeight: '$1',
|
|
color: 'var(--text-secondary)',
|
|
} as const
|
|
|
|
/* ----------------------------------------------------------------- parts --- */
|
|
|
|
/** The section shell every band below shares, so none can drift from the rest. */
|
|
function Band({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Box
|
|
render="section"
|
|
borderTopWidth={1}
|
|
borderColor="var(--neutral-900)"
|
|
px="$4"
|
|
py="$12"
|
|
$sm={{ px: '$5', py: '$14' }}
|
|
$lg={{ px: '$6' }}
|
|
>
|
|
<Box width="100%" maxW={1024} mx="auto">
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* One band's headline. One size, one weight, everywhere — and the size is the
|
|
* `title` ROLE rather than a scale written here, so this page cannot state a
|
|
* rung the rest of the site does not have.
|
|
*/
|
|
function Head({ children }: { children: React.ReactNode }) {
|
|
return <Title>{children}</Title>
|
|
}
|
|
|
|
/** The line under a headline. */
|
|
function Lead({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Lede mt={24} maxW={672}>
|
|
{children}
|
|
</Lede>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The paragraph that follows a lead — the same measure and leading, one step
|
|
* further from the heading. Seven bands wrote it out identically.
|
|
*/
|
|
function Note({ top = 32, children }: { top?: number; children: React.ReactNode }) {
|
|
return (
|
|
<Text render="p" display="block" mt={top} maxW={672} fontSize="$4" lineHeight="1.5234rem" color="var(--muted-foreground)">
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A link out of a band — one shape, so twelve of them read as one page.
|
|
*
|
|
* `tap`, not a stated 44px: the floor is pointer-conditional site policy
|
|
* (24px always, 44px under a coarse pointer), and a `min-h-11` per link is the
|
|
* per-call-site copy of it that globals.css records as the defect.
|
|
*/
|
|
function Further({ href, children }: { href: string; children: React.ReactNode }) {
|
|
return (
|
|
<XStack mt={40}>
|
|
<More href={href} {...tap}>
|
|
{children} <ArrowRight size={16} />
|
|
</More>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/** A quieter way out beside a `Further`: the same shape, at book weight and in the muted ink. */
|
|
function Aside({ href, children }: { href: string; children: React.ReactNode }) {
|
|
return (
|
|
<More
|
|
href={href}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
{...tap}
|
|
mt={40}
|
|
fontWeight="400"
|
|
color="var(--muted-foreground)"
|
|
hoverStyle={{ color: '$ink' }}>
|
|
{children}
|
|
</More>
|
|
)
|
|
}
|
|
|
|
/** A run of two links out, each carrying its own top margin. */
|
|
function Outs({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<XStack flexWrap="wrap" items="center" columnGap={32}>
|
|
{children}
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A command, shown with a prompt glyph that is never copied.
|
|
*
|
|
* The row WRAPS rather than scrolling: a command that runs off the right edge
|
|
* of a phone is a command nobody can read before they run it. `overflow-wrap`
|
|
* at word boundaries, never `anywhere` — the latter broke the image name
|
|
* mid-token at 390px, which is a command nobody can trust.
|
|
*/
|
|
function Command({ value, id }: { value: string; id: string }) {
|
|
return (
|
|
<XStack
|
|
display="inline-flex"
|
|
maxW="100%"
|
|
flexWrap="wrap"
|
|
items="center"
|
|
gap="$3"
|
|
rounded="var(--radius-lg)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
bg="$hover"
|
|
px="$4"
|
|
py={10}
|
|
>
|
|
<Text fontFamily="$mono" fontSize="$2" lineHeight="1.15rem" text="left" color="var(--text-secondary)" $platform-web={{ overflowWrap: 'break-word' }}>
|
|
$ {value}
|
|
</Text>
|
|
<CopyButton value={value} label={`Copy: ${value}`} size={20} id={id} />
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- type --- */
|
|
|
|
/** A cell of a hairline grid: the gap is the rule, so each cell paints the ground. */
|
|
const CELL = { bg: 'var(--surface-card-quiet)', p: '$5' } as const
|
|
|
|
/** A cell's two lines: its name, and what it does. */
|
|
function CellTitle({ render = 'h3', children }: { render?: 'h3' | 'p'; children: React.ReactNode }) {
|
|
return (
|
|
<Text render={render} display="block" fontSize="$4" lineHeight="1.4rem" fontWeight="500" color="$ink">
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
function CellLine({ top = 12, children }: { top?: number; children: React.ReactNode }) {
|
|
return (
|
|
<Text render="p" display="block" mt={top} fontSize="$2" lineHeight="1.3203rem" color="var(--muted-foreground)">
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A grid whose GAP is the rule between its cells: one hairline border round the
|
|
* whole block, a 1px gap filled by the block's own ground, and each cell
|
|
* painting the page black over it. Four bands draw it.
|
|
*/
|
|
function Cells({ columns, children }: { columns: 2 | 3 | 4; children: React.ReactNode }) {
|
|
return (
|
|
<Box
|
|
mt="$9"
|
|
overflow="hidden"
|
|
rounded="var(--radius-2xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
bg="var(--neutral-800)"
|
|
>
|
|
<Grid columns={{ min: 240, max: columns }} gap={1}>
|
|
{children}
|
|
</Grid>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ hero --- */
|
|
|
|
/**
|
|
* The claim, the reach, and one thing a developer can run.
|
|
*
|
|
* THE COPY ARRIVES. Each element rises on @hanzo/ui's `rise`, one 70ms beat
|
|
* after the one before it (`data-beat`) — quick enough that a reader who scrolls
|
|
* immediately is not waiting on it. It is a keyframe keyed on `data-motion`
|
|
* rather than a prop on a motion component, so it starts at FIRST PAINT instead
|
|
* of at hydration: an entrance declared in JavaScript ships `opacity: 0` in the
|
|
* HTML, and the largest element on the page has no business being invisible
|
|
* until a bundle lands. Under reduced motion there is no animation and no
|
|
* delay, so nothing waits hidden.
|
|
*/
|
|
function Hero({ models }: { models: string | null }) {
|
|
/** The nth thing to arrive: @hanzo/ui's `rise`, 70ms after the one before.
|
|
* The motion sheet turns the whole entrance off under reduced motion, so a
|
|
* reader who asked for stillness gets the page at once, delays and all. */
|
|
const rise = (i: number) => ({ 'data-motion': 'rise', '$platform-web': { animationDelay: `${i * 70}ms` } }) as const
|
|
|
|
// The model count leads the strip when the gateway has answered and is simply
|
|
// absent when it has not. A row that says "models" with no number is a row
|
|
// that spends a reader's attention on nothing.
|
|
const strip = [
|
|
models,
|
|
'GPUs',
|
|
'Functions',
|
|
'Containers',
|
|
'Databases',
|
|
'Observability',
|
|
'One API',
|
|
].filter(Boolean) as string[]
|
|
|
|
return (
|
|
// `svh`, not `vh`: mobile browser chrome makes `100vh` taller than the
|
|
// screen and the fold overflows by exactly the address bar.
|
|
<Box
|
|
render="section"
|
|
display="flex"
|
|
flexDirection="row"
|
|
minH="100svh"
|
|
width="100%"
|
|
items="center"
|
|
px="$4"
|
|
py="$6"
|
|
$sm={{ px: '$5', py: '$10' }}
|
|
/* From lg the copy starts under the header's wordmark: the header sets
|
|
its items a page gutter and 8px in, and so does this. At $6 the
|
|
heading sat 32px from the window while the logo above it sat 66. */
|
|
$lg={{ px: 'calc(var(--page-gutter) + 8px)' }}
|
|
>
|
|
{/* Two columns from lg, and the tracks change SHAPE at xl rather than
|
|
appearing there. Reading runs left to right, so the picture belongs
|
|
beside the copy at every width that can hold both.
|
|
|
|
At lg they share the width in proportion — 1fr to 1.1fr, which at 1024
|
|
is 434 against 478. Naming the fixed 34rem measure this early is what
|
|
used to force a stack: the first track takes its 544 whatever is left,
|
|
so the picture got 360 and a 360x203 still beside a full column of
|
|
type is worse than stacking.
|
|
|
|
From xl the measure is capped instead. Past ~34rem a line is wider
|
|
than anyone finishes reading, so every pixel beyond it belongs to the
|
|
picture — 544 against 776 at 1440. Keeping the proportion there would
|
|
grow both and leave the copy unreadable. */}
|
|
<Box
|
|
display="grid"
|
|
width="100%"
|
|
maxW={1600}
|
|
mx="auto"
|
|
items="center"
|
|
gap="$6"
|
|
gridTemplateColumns="minmax(0,1fr)"
|
|
$sm={{ gap: '$8' }}
|
|
$lg={{ gridTemplateColumns: 'minmax(0,1fr) minmax(0,1.1fr)' }}
|
|
$xl={{ gridTemplateColumns: 'minmax(0,34rem) minmax(0,1fr)', gap: '$9' }}
|
|
$xxl={{ maxW: 1800 }}
|
|
>
|
|
<YStack>
|
|
<Text render="p" display="block" {...rise(0)} fontSize="$2" lineHeight="1.15rem" fontWeight="500" textTransform="uppercase" letterSpacing={1.82} color="var(--muted-foreground)">
|
|
Hanzo Cloud
|
|
</Text>
|
|
|
|
<Display {...rise(1)} mt={20}>
|
|
{/* SOLID INK. A white -> neutral gradient clipped to the glyphs
|
|
makes the END of the line its dimmest point, so the last word
|
|
reads as a render that never finished. Hanzo's ink is
|
|
paper-white.
|
|
|
|
Four words, and they are the category rather than a feeling.
|
|
What this is IS the surprise: a cloud whose primitives assume
|
|
there is a model in the application.
|
|
|
|
The hyphen is U+2011, which does not offer a break. An ordinary
|
|
one does, and at 434px of measure the line broke after it: "The
|
|
AI-" over "native cloud.", which reads as a word cut in half. */}
|
|
The AI‑native cloud.
|
|
</Display>
|
|
|
|
<Lede {...rise(2)} mt={28} maxW={672}>
|
|
Run models, agents, applications, data and infrastructure through one
|
|
operating plane.
|
|
</Lede>
|
|
|
|
<Text render="p" display="block" {...rise(3)} mt={20} maxW={672} fontSize="$3" lineHeight="1.4219rem" color="var(--muted-foreground)">
|
|
Kubernetes-native. Fully observable. Open source. Run it on Hanzo
|
|
infrastructure or infrastructure you control.
|
|
</Text>
|
|
|
|
<XStack {...rise(4)} mt={36} flexWrap="wrap" items="center" gap={12}>
|
|
<Action href={KEYS} fill>
|
|
Start building <ArrowRight size={16} />
|
|
</Action>
|
|
<Action href="/products">
|
|
Explore the cloud
|
|
</Action>
|
|
</XStack>
|
|
|
|
{/* The reach, as a strip rather than a paragraph. The one number in it
|
|
is asked of the gateway at read time rather than baked. */}
|
|
<Text render="p" display="block" {...rise(5)} mt={32} fontSize="$2" color="var(--muted-foreground)" $platform-web={{ lineHeight: '1.75' }}>
|
|
{strip.map((item, i) => (
|
|
<Text key={item} color="inherit">
|
|
{i > 0 && <Text color="var(--neutral-700)"> · </Text>}
|
|
{item}
|
|
</Text>
|
|
))}
|
|
</Text>
|
|
|
|
{/* ONE developer artifact on the fold, and it is the whole platform
|
|
running on a laptop. The tag resolves on GHCR and the console is
|
|
at :8080 with the API under /v1 on the same origin. */}
|
|
<YStack {...rise(6)} mt={28}>
|
|
<Command value={DEPLOY} id="deploy-cloud" />
|
|
</YStack>
|
|
</YStack>
|
|
|
|
{/* THE SECOND COLUMN. Below lg there is one track and it simply follows
|
|
the copy at full width, which is the only honest answer on a phone —
|
|
two tracks there give each about 170px.
|
|
|
|
The console, running. A reader deciding whether to put their work
|
|
here wants to see the thing they will be looking at every day, not a
|
|
diagram of how it fits together — and drawn rather than filmed, so
|
|
it reflows here, answers a screen reader, and can be opened. */}
|
|
<Box {...rise(7)} minW={0} mt="$9" $lg={{ mt: 0 }}>
|
|
<Surface name="Console" rail="apps" here="console" />
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- whole --- */
|
|
|
|
/**
|
|
* The shape of an AI application, and the fact that one system holds all of it.
|
|
*
|
|
* This is where the page used to state its inventory — the categories, then
|
|
* every product in them. An inventory answers "what exists"; a reader standing
|
|
* here is asking "will my application fit". So the stack is the WORKLOAD, not
|
|
* the catalog: the six things a modern application is made of, in the order
|
|
* they sit on each other.
|
|
*/
|
|
const WHOLE: [string, string][] = [
|
|
['Model', 'Inference, embeddings, routing, fallbacks.'],
|
|
['Agent', 'Tools, memory, sandboxes, orchestration.'],
|
|
['Application', 'Functions, containers, machines, endpoints.'],
|
|
['Data', 'Relational, vector, objects, cache.'],
|
|
['Infrastructure', 'Clusters, networking, regions, capacity.'],
|
|
['Operations', 'Identity, secrets, traces, billing.'],
|
|
]
|
|
|
|
function Whole() {
|
|
return (
|
|
<Band>
|
|
<Head>One cloud for the whole AI application.</Head>
|
|
<Lead>
|
|
Not a model API beside a container host beside a database beside a
|
|
tracing vendor. One system, with the parts already introduced to each
|
|
other.
|
|
</Lead>
|
|
|
|
<Box mt="$9" overflow="hidden" rounded="var(--radius-2xl)" borderWidth={1} borderColor="var(--neutral-800)">
|
|
<YStack render="ol" m={0} p={0}>
|
|
{WHOLE.map(([label, parts], i) => (
|
|
<Box
|
|
key={label}
|
|
render="li"
|
|
display="grid"
|
|
/* One track on a phone with the label's own lead above the line;
|
|
a 208px label column beside it from `$sm`, where the two are one
|
|
row and the row gap no longer applies. */
|
|
gridTemplateColumns="minmax(0,1fr)"
|
|
columnGap={32}
|
|
rowGap={8}
|
|
items="baseline"
|
|
borderTopWidth={i === 0 ? 0 : 1}
|
|
borderTopColor="var(--neutral-900)"
|
|
px={20}
|
|
py={20}
|
|
$sm={{
|
|
gridTemplateColumns: '13rem minmax(0,1fr)',
|
|
px: 28,
|
|
py: '$5',
|
|
}}
|
|
>
|
|
<Text fontFamily="$mono" fontSize="$1" lineHeight="$1" textTransform="uppercase" letterSpacing={1.54} color="var(--muted-foreground)">
|
|
{label}
|
|
</Text>
|
|
<Text display="block" fontSize="$4" lineHeight="1.4rem" color="var(--text-secondary)">
|
|
{parts}
|
|
</Text>
|
|
</Box>
|
|
))}
|
|
</YStack>
|
|
</Box>
|
|
|
|
<Note>
|
|
Everything shares the same organization, project, identity, policy,
|
|
usage and operational context.
|
|
</Note>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- reach --- */
|
|
|
|
/**
|
|
* One control plane, more than one place to run.
|
|
*
|
|
* WHAT IS CLAIMED, and what is carefully not. Hanzo's own fleet runs on one
|
|
* provider; the other clouds are reached with credentials an org supplies for
|
|
* ITSELF, and a cluster is attached rather than provisioned. So the claim is
|
|
* "attach your own account or your own cluster" — which is what the code does —
|
|
* and never "we run your workloads across eight clouds", which nothing here
|
|
* would back.
|
|
*
|
|
* VISOR IS NAMED as the runtime and not as the product. The customer-facing
|
|
* names are Machines, GPUs, Clusters and Kubernetes — each of which passes this
|
|
* site's own reachability gate into the catalog — and Visor is what serves
|
|
* them, which is how the apex site already refers to it. The link goes to
|
|
* `/visor` here, never to a docs page: that path does not resolve.
|
|
*/
|
|
const REACH: [string, string][] = [
|
|
['Hanzo Cloud', 'Capacity we operate and meter, provisioned by the call.'],
|
|
['Your own cloud account', 'Your credentials, your machines, the same control plane over them.'],
|
|
['Your own cluster', 'Attach Kubernetes you already run and address it like any other.'],
|
|
]
|
|
|
|
function Reach() {
|
|
return (
|
|
<Band>
|
|
<Head>The cloud is bigger than one provider.</Head>
|
|
<Lead>
|
|
One control plane, different infrastructure underneath it. The API, the
|
|
console, the identities and the bill do not change when the machines do.
|
|
</Lead>
|
|
|
|
<Cells columns={3}>
|
|
{REACH.map(([title, line]) => (
|
|
<Box key={title} {...CELL} $sm={{ p: 28 }}>
|
|
<CellTitle>{title}</CellTitle>
|
|
<CellLine>{line}</CellLine>
|
|
</Box>
|
|
))}
|
|
</Cells>
|
|
|
|
<Note>
|
|
Machines, GPUs, clusters and Kubernetes are one surface whichever of
|
|
those it lands on. Visor is the runtime underneath them, and it meters
|
|
every provider against the org that asked.
|
|
</Note>
|
|
|
|
<Further href="/visor">Compute across your own accounts</Further>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* --------------------------------------------------------------- models --- */
|
|
|
|
function Models() {
|
|
return (
|
|
<Band>
|
|
<Head>Every model. One cloud interface.</Head>
|
|
<Lead>
|
|
Let Enso choose when you want the best answer for the job. Name a model
|
|
when you want exactly that one, every time. Run open weights when you
|
|
want to hold them yourself.
|
|
</Lead>
|
|
|
|
<Note>
|
|
Whichever you pick, auth, budgets, routing, fallbacks, usage and
|
|
observability stay in one system — so changing your mind about a model
|
|
is a string, not a migration.
|
|
</Note>
|
|
|
|
<XStack mt="$7" flexWrap="wrap" items="center" columnGap="$3" rowGap="$2">
|
|
{['OpenAI-compatible', '·', 'Anthropic-compatible'].map((w, i) => (
|
|
<Text key={i} fontFamily="$mono" fontSize="$2" lineHeight="1.15rem" color={w === '·' ? 'var(--neutral-700)' : 'var(--text-secondary)'}>
|
|
{w}
|
|
</Text>
|
|
))}
|
|
</XStack>
|
|
|
|
<Outs>
|
|
<Further href="/models">Every model we serve</Further>
|
|
<Further href="/agents">Build and run agents</Further>
|
|
</Outs>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- data --- */
|
|
|
|
/**
|
|
* The data plane, one line per primitive.
|
|
*
|
|
* The LIST is the catalog's, not this file's: `cloudCategories` is read from
|
|
* the commerce snapshot and filtered to what publishes, so a primitive renamed
|
|
* upstream is renamed here and one withdrawn disappears. This file owns only
|
|
* the sentence under each name, keyed by slug — a primitive with no sentence
|
|
* still renders, with the catalog's own descriptor.
|
|
*/
|
|
const DATA: Record<string, string> = {
|
|
sql: 'Relational, per project and per tenant.',
|
|
vector: 'Embeddings stored and searched where they are used.',
|
|
kv: 'Cache and queues at request latency.',
|
|
s3: 'Objects, signed URLs, lifecycle rules.',
|
|
docdb: 'Documents on the MongoDB wire protocol.',
|
|
datastore: 'Columnar analytics over everything you collected.',
|
|
base: 'An application backend — records, rules, realtime.',
|
|
memory: 'What an agent is allowed to remember between runs.',
|
|
}
|
|
|
|
function Data() {
|
|
const plane = cloudCategories.find((c) => c.id === 'data')
|
|
if (!plane) return null
|
|
|
|
return (
|
|
<Band>
|
|
<Head>The data primitives AI applications actually use.</Head>
|
|
<Lead>
|
|
Not one database with an extension bolted on. Each primitive is the
|
|
right shape for the job, and they share a project, a key and a bill.
|
|
</Lead>
|
|
|
|
<View display="grid" gridTemplateColumns={tracks({ min: 280, max: 2 }, '40px')} columnGap={40} rowGap={32} mt={56}>
|
|
{plane.items.map((item) => (
|
|
<YStack key={item.slug} minW={0}>
|
|
<Text render="h3" display="block" fontSize="$4" lineHeight="1.4rem" fontWeight="500">
|
|
<More href={item.href} fontSize="$4" color="$ink">
|
|
{item.title}
|
|
</More>
|
|
</Text>
|
|
<CellLine top={8}>{DATA[item.slug] ?? item.desc}</CellLine>
|
|
</YStack>
|
|
))}
|
|
</View>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* -------------------------------------------------------------- network --- */
|
|
|
|
function Network() {
|
|
return (
|
|
<Band>
|
|
<Head>Everything connects without becoming your problem.</Head>
|
|
<Lead>
|
|
Services find each other. Traffic reaches them over TLS on a name you
|
|
chose. The edges are closed until you open one.
|
|
</Lead>
|
|
|
|
<Note>
|
|
The gateway fronts every API, DNS answers for your names, and routing
|
|
between services is something the cloud does rather than something you
|
|
write.
|
|
</Note>
|
|
|
|
<Further href="/network">The networking plane</Further>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* -------------------------------------------------------------- observe --- */
|
|
|
|
/**
|
|
* The differentiator, given a whole band rather than a card in a grid.
|
|
*
|
|
* A model call, an agent step, a query and a function invocation are four
|
|
* different products almost everywhere else, which means four different traces
|
|
* and no way to add them up. Here they are one request, so the sentence that
|
|
* matters is short enough to be a headline of its own.
|
|
*/
|
|
function Observe() {
|
|
return (
|
|
<Band>
|
|
<Head>See the entire system, not separate products.</Head>
|
|
<Lead>
|
|
One request, from the edge through an agent, a model, a tool, a database
|
|
and a function — and what every hop of it cost.
|
|
</Lead>
|
|
|
|
<Title render="p" quiet mt={56}>
|
|
One trace. End to end.
|
|
</Title>
|
|
|
|
<Note top={24}>
|
|
Logs, metrics, traces, evaluations and spend are read from the same
|
|
request, so “why was that answer slow” and “why was
|
|
that answer expensive” are the same question with the same answer.
|
|
</Note>
|
|
|
|
<Outs>
|
|
<Further href="/o11y">Tracing, metrics and evaluation</Further>
|
|
<Aside href={STATUS}>
|
|
Live status, in public <ArrowRight size={16} />
|
|
</Aside>
|
|
</Outs>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ------------------------------------------------------------- security --- */
|
|
|
|
function Security() {
|
|
return (
|
|
<Band>
|
|
<Head>Security is part of the cloud.</Head>
|
|
<Lead>
|
|
Every person, agent and workload has an identity. Permissions follow the
|
|
resource rather than the caller. Secrets stay scoped to the environment
|
|
that needs them. Actions stay attributable to whoever took them.
|
|
</Lead>
|
|
|
|
<Note>
|
|
None of that is a product you add afterwards — an agent gets an identity
|
|
because it runs here, and the trail is written because the call went
|
|
through the same entry point as everything else.
|
|
</Note>
|
|
|
|
<Further href="/security">How identity, keys and audit work</Further>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ api --- */
|
|
|
|
/**
|
|
* The one API, said as the addresses themselves.
|
|
*
|
|
* EVERY PATH IS THE CATALOG'S. `getPrimitive` reads the commerce snapshot,
|
|
* which `scripts/sync-catalog.mjs` gates on the served OpenAPI document — so a
|
|
* path printed here is one the platform answers, and a product whose path
|
|
* stops answering drops out of this list rather than becoming a 404 in a
|
|
* display font.
|
|
*/
|
|
const SURFACE = ['models', 'agents', 'functions', 'sql', 'vector', 's3']
|
|
|
|
function Api() {
|
|
const paths = SURFACE.map((slug) => getPrimitive(slug)?.api).filter(Boolean) as string[]
|
|
if (!paths.length) return null
|
|
|
|
return (
|
|
<Band>
|
|
<Head>One API. Every capability.</Head>
|
|
|
|
<Box
|
|
mt="$8"
|
|
overflow="hidden"
|
|
rounded="var(--radius-2xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
bg="$sunken"
|
|
>
|
|
<Box
|
|
overflowX="auto"
|
|
px={20}
|
|
py="$5"
|
|
$sm={{ px: '$6', py: '$6' }}
|
|
>
|
|
{/* The API surface, as the addresses themselves, on a 2 leading. */}
|
|
<Text render="ul" display="block" m={0} p={0} fontFamily="$mono" fontSize="$2" color="var(--text-secondary)" $platform-web={{ lineHeight: '2' }}>
|
|
{paths.map((path) => (
|
|
<Text key={path} render="li" display="block" whiteSpace="nowrap" color="inherit">
|
|
<Text color="var(--text-tertiary)">api.hanzo.ai</Text>
|
|
{path}
|
|
</Text>
|
|
))}
|
|
<Text render="li" display="block" whiteSpace="nowrap" color="var(--text-tertiary)">
|
|
api.hanzo.ai/v1/…
|
|
</Text>
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Note top={40}>
|
|
Same key. Same organization. Same project. Same policy model. Same usage
|
|
ledger.
|
|
</Note>
|
|
|
|
<Title render="p" quiet mt={24}>
|
|
The interface changes. The system doesn’t.
|
|
</Title>
|
|
|
|
{tour.beats.length > 0 && (
|
|
<>
|
|
<Text render="p" display="block" mt={48} maxW={672} fontSize="$3" lineHeight="1.4219rem" color="var(--muted-foreground)">
|
|
{spell(tour.beats.length)} operations, one key, no glue. Each is
|
|
checked against the published OpenAPI document on every build, and a
|
|
beat the document does not carry is dropped rather than drawn.
|
|
</Text>
|
|
<Box mt="$6">
|
|
<Tour />
|
|
</Box>
|
|
</>
|
|
)}
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* --------------------------------------------------------------- deploy --- */
|
|
|
|
function Deploy() {
|
|
return (
|
|
<Band>
|
|
<Head>Push code. Get a production system.</Head>
|
|
<Lead>
|
|
No separate build service to wire up. No container registry to stand up.
|
|
No secret product to buy. No logging to set up afterwards.
|
|
</Lead>
|
|
|
|
<Note>
|
|
What comes back is a running system with an address, a certificate, its
|
|
secrets, its metrics, its logs and its share of the bill — because those
|
|
were never separate purchases.
|
|
</Note>
|
|
|
|
<Outs>
|
|
<Further href="/registry">Builds, releases and rollback</Further>
|
|
<Further href="/cli">The CLI</Further>
|
|
</Outs>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ------------------------------------------------------------- portable --- */
|
|
|
|
/**
|
|
* The exit, offered rather than hidden.
|
|
*
|
|
* Both commands are verified. `hanzo.sh` answers with a POSIX installer, and
|
|
* `serve` is a registered subcommand of the shipped CLI that takes `cloud` for
|
|
* the whole API. `hanzo up` — which several other pages on this site print — is
|
|
* NOT a command the CLI has; an unrecognised first word is read as a task for
|
|
* the coding agent, so it fails by doing something else.
|
|
*/
|
|
function Portable() {
|
|
return (
|
|
<Band>
|
|
<Head>Your cloud shouldn’t lock you in.</Head>
|
|
<Lead>
|
|
The platform is open source, and the image we operate is the image you
|
|
can pull. Same API, same console, same data — on your cluster, in your
|
|
network, or on a laptop on a plane.
|
|
</Lead>
|
|
|
|
<YStack mt="$7" items="flex-start" gap="$3">
|
|
<Command value={INSTALL} id="install-cli" />
|
|
<Command value={SERVE} id="serve-cloud" />
|
|
</YStack>
|
|
|
|
{/* No margin on the row: `Further` and the source link each carry their own
|
|
40px, and a container that adds a second one spaces this band twice
|
|
as far off the commands above as every other band is spaced. */}
|
|
<Outs>
|
|
<Further href="/open-source">What is open, and under which license</Further>
|
|
<Aside href={GH}>
|
|
<Github size={16} /> View source
|
|
</Aside>
|
|
</Outs>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- price --- */
|
|
|
|
/**
|
|
* The SHAPE of the bill, and one link to the rates.
|
|
*
|
|
* Plan mechanics are `/pricing`'s job. This page used to render the whole
|
|
* ladder — four plan cards, per-seat team pricing, minimum seats — which is a
|
|
* procurement question asked of a reader who is still deciding whether the
|
|
* thing is for them. What belongs here is what you are actually billed for.
|
|
*/
|
|
const PRICE: [string, string][] = [
|
|
['Models', 'by usage'],
|
|
['Compute', 'by execution'],
|
|
['Storage', 'by consumption'],
|
|
['Managed services', 'by usage'],
|
|
]
|
|
|
|
function Price() {
|
|
return (
|
|
<Band>
|
|
<Head>Pay for what runs.</Head>
|
|
<Lead>
|
|
Nothing is billed for existing. A function that was not called and a
|
|
machine that was not running cost what they did — nothing.
|
|
</Lead>
|
|
|
|
<Cells columns={4}>
|
|
{PRICE.map(([what, how]) => (
|
|
<Box key={what} {...CELL}>
|
|
<CellTitle render="p">{what}</CellTitle>
|
|
<Text render="p" display="block" mt={8} fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
|
|
{how}
|
|
</Text>
|
|
</Box>
|
|
))}
|
|
</Cells>
|
|
|
|
<Note>
|
|
One organization balance and one usage ledger behind all of it, so every
|
|
line traces back to a request somebody made.
|
|
</Note>
|
|
|
|
<Further href="/pricing">Every plan and every rate</Further>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* -------------------------------------------------------------- explore --- */
|
|
|
|
/**
|
|
* The catalog, at the bottom, for the reader who came to browse.
|
|
*
|
|
* SEVEN, and they are the planes a reader builds ON. Apps and Web3 are not
|
|
* peers of Compute and Data — apps are built on this cloud, so listing them
|
|
* beside the primitives they consume tells a reader the wrong thing about what
|
|
* the cloud is. Both are still one click away in the full catalog, which is
|
|
* what `/products` is for.
|
|
*
|
|
* The names are the CATALOG'S names, not this file's. A card headed one thing
|
|
* that opens a page headed another is exactly the drift the catalog snapshot
|
|
* exists to prevent — and each card's href is a route the sync resolved
|
|
* against the filesystem.
|
|
*/
|
|
const EXPLORE = ['ai', 'compute', 'data', 'dev', 'observe', 'security', 'network']
|
|
|
|
function Explore() {
|
|
const shown = EXPLORE.map((id) => cloudCategories.find((c) => c.id === id)).filter(
|
|
Boolean,
|
|
) as typeof cloudCategories
|
|
|
|
if (!shown.length) return null
|
|
|
|
return (
|
|
<Band>
|
|
<Head>Explore the cloud.</Head>
|
|
<Lead>Every plane, and what is in it.</Lead>
|
|
|
|
<Cells columns={3}>
|
|
{shown.map((cat) => {
|
|
const Icon = cat.icon
|
|
return (
|
|
<Box key={cat.id} {...CELL}>
|
|
<Text render="h3" display="block" fontSize="$4" lineHeight="1.4rem" fontWeight="500">
|
|
<More href={MOVED[`/products/${cat.id}`]} gap={10} fontSize="$4" color="$ink">
|
|
<Text display="inline-flex" shrink={0} color="var(--muted-foreground)">
|
|
<Icon size={16} />
|
|
</Text>
|
|
{cat.title}
|
|
</More>
|
|
</Text>
|
|
<Text render="p" display="block" mt={16} fontSize="$2" $platform-web={{ lineHeight: '1.75' }}>
|
|
{/* Five, because a card is a taste of a plane rather than an
|
|
index of it — Observe carries seventeen products and a card
|
|
listing all of them is the wall this page just stopped
|
|
being. */}
|
|
{cat.items.slice(0, 5).map((item, i) => (
|
|
<Text key={item.slug} color="inherit">
|
|
{i > 0 && <Text color="var(--neutral-700)"> · </Text>}
|
|
<More href={item.href} {...tap} fontWeight="400" color="var(--muted-foreground)">
|
|
{item.title}
|
|
</More>
|
|
</Text>
|
|
))}
|
|
</Text>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Cells>
|
|
|
|
<Further href="/products">View the complete catalog</Further>
|
|
</Band>
|
|
)
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- cta --- */
|
|
|
|
function Close() {
|
|
return (
|
|
<Box
|
|
render="section"
|
|
position="relative"
|
|
overflow="hidden"
|
|
borderTopWidth={1}
|
|
borderColor="var(--neutral-900)"
|
|
px="$4"
|
|
py={112}
|
|
$sm={{ px: '$5', py: '$14' }}
|
|
$lg={{ px: '$6' }}
|
|
>
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={768} mx="auto" $platform-web={{ textAlign: 'center' }}>
|
|
<Title>Build on a cloud designed for AI.</Title>
|
|
<Lede mt={20} maxW={576} mx="auto">
|
|
Models. Agents. Compute. Data. Security. Observability.
|
|
</Lede>
|
|
<Lede mt={12} maxW={576} mx="auto">
|
|
One API from prototype to production.
|
|
</Lede>
|
|
|
|
<XStack mt={36} flexWrap="wrap" items="center" justify="center" gap="$3">
|
|
<Action href={KEYS} fill>
|
|
Start building <ArrowRight size={16} />
|
|
</Action>
|
|
<Action href={DOCS} target="_blank" rel="noreferrer noopener">
|
|
Read the docs
|
|
</Action>
|
|
</XStack>
|
|
|
|
<Text render="p" display="block" mt={28} fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
|
|
Or run Hanzo Cloud on infrastructure you control.
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
/* --------------------------------------------------------------- page ---- */
|
|
|
|
/**
|
|
* ProofStrip's rule, applied to a count asked at read time: floor to the
|
|
* hundred so the phrase stays true between one read and the next, and state a
|
|
* count below a hundred exactly rather than rounding it away to zero.
|
|
*/
|
|
function callable(n: number): string {
|
|
return n >= 100
|
|
? `${Math.floor(n / 100) * 100}+ callable models`
|
|
: `${n} callable models`
|
|
}
|
|
|
|
export default function CloudLanding() {
|
|
// How many models there are, asked of the price list when the page opens
|
|
// (lib/api). Until it answers — and if it never does — the phrase that would
|
|
// quote a number is not drawn at all.
|
|
const n = ready(useSummary())?.totalModels ?? 561
|
|
|
|
return (
|
|
<>
|
|
<Hero models={n ? callable(n) : null} />
|
|
<Whole />
|
|
{/* Compute. `Workloads` is the band — one runtime, six kinds of work,
|
|
with the three isolations shown in the API's own words. */}
|
|
<Workloads />
|
|
<Reach />
|
|
<Models />
|
|
<Data />
|
|
<Network />
|
|
<Observe />
|
|
<Security />
|
|
<Api />
|
|
<Deploy />
|
|
<Portable />
|
|
<Price />
|
|
<Explore />
|
|
<Close />
|
|
</>
|
|
)
|
|
}
|