Files
zeekay bba4e5ca40 A product page ends on one call: the block above the closing keeps the docs and the neighbours and drops its own Get your API key
Every product page ended on "Start building with X" and a filled Get your
API key, then one screen later on the layout's "Build what's next." and a
filled Try Hanzo, in a different button shape: two closing calls a reader
had to choose between. StartBuilding becomes Related: the product's docs
link and its related capabilities, no heading and no button, so the
closing call below is the one way in. And the open-source row no longer
shows a "0 stars" pill, which read as an abandoned repository.
2026-10-18 16:25:16 -07:00

582 lines
20 KiB
TypeScript

"use client"
/**
* /tasks — durable workflow execution, and what it costs.
*
* NO TAILWIND. The six section frames are `components/marketing/band`'s `Band`
* and `Head` — the rule, the rhythm, the gutter, the measure and the centred
* head this page wrote out by hand at each of them — the roles carry the type
* and the controls (`Display`, `Claim`, `Lede`, `Chip`, `Action`, `Leaf`), and
* everything left is gui props over @hanzo/design's tokens.
*
* Three things the swap moves, each deliberate:
*
* - the rules take `Band`'s `--neutral-900` hairline rather than this page's
* `--border`, which is `rgb(255 255 255 / .10)` — the same line to within a
* value of grey, and now the one line the whole site draws.
* - `Claim` and `Lede` step at 640 where `md:` stepped at 768, and each sits
* on its role's rung rather than the one this page typed.
* - `Action` is the one button shape at its two weights, and `Leaf` is the
* card whose edge lifts, its hover a prop beside its resting edge.
*
* The hero is NOT a Band: it is a 90vh centred stage rather than a section of
* rhythm. A plan card keeps `flexBasis="0%"` on its middle pane rather than
* gui's `flex={1}`, which writes a definite `0px` basis — the same `flex: 1 1 0%`
* the utility meant, said where it cannot be mistaken for a height.
*/
import type { ReactNode } from "react"
import { motion } from "@/components/motion"
import { ProductFooter } from "@/components/products/ProductFooter"
import {
ListChecks,
ArrowRight,
RotateCcw,
Shield,
Clock,
GitBranch,
Radio,
History,
Lock,
Users,
Bot,
ShoppingCart,
Workflow,
CalendarClock,
UserCheck,
Check,
} from "lucide-react"
import { Box, XStack, YStack } from '@hanzo/ui'
import { Grid } from '@hanzo/ui/grid'
import { Band, Head } from '@/components/marketing/band'
import { AgentPrompt } from '@/components/product/AgentPrompt'
import { Related } from '@/components/product/Related'
import { product } from '@/lib/product'
import { Action, Chip, Claim, Display, Lede, Leaf, Line, type LineProps } from '@hanzo/ui/marketing'
/** What the two shared blocks say about this product: the audited command, the
* route the catalog proves it by, and the docs page or null. One table answers
* for every product page, so this one cannot say something its neighbours do
* not. */
const PRODUCT = product('tasks')
/** How narrow a column may get before a grid drops one, per shape. */
const DECK = { min: 304, max: 3 } as const
const DUO = { min: 320, max: 2 } as const
const PLANS = { min: 240, max: 4 } as const
const STATS = { min: 150, max: 4 } as const
/** The inks this page draws body copy and marks in. */
const SOFT = 'color-mix(in oklab, var(--foreground) 80%, transparent)'
const WASH = 'color-mix(in oklab, var(--primary) 10%, transparent)'
const TILE = 'color-mix(in oklab, var(--secondary) 50%, transparent)'
const TILE_SOFT = 'color-mix(in oklab, var(--secondary) 30%, transparent)'
/** The card whose edge lifts on approach — `Leaf`, at this page's two washes. */
const LEAF = { p: 24, borderColor: 'var(--border)', hoverStyle: { borderColor: 'var(--neutral-600)' }, bg: TILE } as const
const LEAF_SOFT = { ...LEAF, bg: TILE_SOFT } as const
/** The hairline tile a stat and a plan sit on, and the plan that is lit. A
* `color-mix()` ink is not a value gui's colour props type, so it rides
* `$platform-web`, which gui still writes as an atomic class. */
const TILE_BOX = { rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', '$platform-web': { backgroundColor: TILE } } as const
const LIT = {
rounded: 'var(--radius-xl)',
borderWidth: 2,
'$platform-web': { backgroundColor: WASH, borderColor: 'color-mix(in oklab, var(--foreground) 20%, transparent)' },
} as const
/** A card's heading and the paragraph under it. */
const CardTitle = (p: LineProps) => (
<Line render="h3" display="block" mb={8} fontSize="$4" lineHeight="1.5556" fontWeight="600" color="var(--foreground)" {...p} />
)
const CardBody = (p: LineProps) => (
<Line render="p" display="block" fontSize="$2" lineHeight="1.4286" color="var(--muted-foreground)" {...p} />
)
/** The mark's plate, at the two sizes this page draws it. */
const Plate = ({ size, children }: { size: number; children: ReactNode }) => (
<XStack items="center" justify="center" mb={16} width={size} height={size} shrink={0} rounded="var(--radius-lg)" $platform-web={{ backgroundColor: WASH }}>
{children}
</XStack>
)
/** The source frame: a card whose top bar carries three lamps and a file name. */
const FRAME = { overflow: 'hidden', rounded: 'var(--radius-xl)', borderWidth: 1, borderColor: 'var(--border)', bg: 'var(--secondary)' } as const
function FrameBar({ file }: { file: string }) {
return (
<XStack items="center" gap={8} px={16} py={8} borderBottomWidth={1} borderColor="var(--border)">
<XStack gap={6}>
{[0, 1, 2].map((i) => (
<Box key={i} width={12} height={12} rounded="var(--radius-full)" bg="var(--neutral-700)" />
))}
</XStack>
<Line ml={8} fontSize="$1" lineHeight="1.3333" color="var(--muted-foreground)">
{file}
</Line>
</XStack>
)
}
/** The code inside a frame. `whiteSpace="pre"` because a gui Text wraps by
* default and a code line scrolls; `marginBlock` is the `1em` a `<pre>` keeps,
* which gui's Text would zero. */
const Source = ({ children, ...p }: LineProps) => (
<Line render="pre" display="block" marginBlock="1em" p={16} overflowX="auto" whiteSpace="pre" fontSize="$2" lineHeight="1.4286" color="inherit" {...p}>
<Line render="code" color={SOFT}>
{children}
</Line>
</Line>
)
/** The filled control this page draws, everywhere it draws one. */
const FILL = { px: 32 } as const
const heroStats = [
{ value: "Durable", label: "Execution" },
{ value: "4 SDKs", label: "Go / Py / TS / Java" },
{ value: "Sagas", label: "Built-in" },
{ value: "Free", label: "25K actions/mo" },
]
const architecture = `
+------------------+ +-------------------+
| Your Code | | Hanzo Tasks |
| | | Server |
| Workflow Def ---------> Scheduler |
| Activity Impl --------> Event History |
| Worker Pool ----------> State Store |
| | | |
+------------------+ +--------+----------+
|
+---------------+---------------+
| | |
+-----------+ +-----------+ +-----------+
| Hanzo SQL | | Hanzo KV | | Hanzo KMS |
| (History) | | (Queues) | | (Secrets) |
+-----------+ +-----------+ +-----------+
`
const pricingTiers = [
{
name: "Free",
price: "$0",
period: "/mo",
description: "For prototyping and small projects",
actions: "25K actions/mo",
namespaces: "1 namespace",
features: [
"Durable execution",
"Automatic retries",
"Web console",
"Community support",
],
cta: "Try Hanzo",
ctaHref: "/login",
highlight: false,
},
{
name: "Pro",
price: "$0.035",
period: "/1K actions",
description: "1M actions included per month",
actions: "1M actions included",
namespaces: "10 namespaces",
features: [
"Everything in Free",
"Scheduled workflows",
"Signals & queries",
"Email support",
],
cta: "Start Pro",
ctaHref: "https://platform.hanzo.ai",
highlight: true,
},
{
name: "Team",
price: "$0.025",
period: "/1K actions",
description: "10M actions included per month",
actions: "10M actions included",
namespaces: "Unlimited",
features: [
"Everything in Pro",
"Multi-tenant isolation",
"IAM integration",
"Priority support",
],
cta: "Start Team",
ctaHref: "https://platform.hanzo.ai",
highlight: false,
},
{
name: "Enterprise",
price: "Custom",
period: "",
description: "Dedicated resources & SLAs",
actions: "Unlimited actions",
namespaces: "Unlimited",
features: [
"Everything in Team",
"Dedicated cluster",
"Custom SLA",
"24/7 support",
],
cta: "Contact Sales",
ctaHref: "/contact-sales",
highlight: false,
},
]
const features = [
{
icon: RotateCcw,
title: "Durable Execution",
description:
"Workflows survive process crashes, server restarts, and network partitions. State is persisted automatically.",
},
{
icon: GitBranch,
title: "Saga Compensation",
description:
"Define compensating actions for every step. Automatic rollback on failure. Distributed transactions done right.",
},
{
icon: Clock,
title: "Scheduled Workflows",
description:
"Cron expressions, delayed starts, and recurring schedules. Timezone-aware. No external scheduler needed.",
},
{
icon: Radio,
title: "Signals & Queries",
description:
"Send signals to running workflows. Query workflow state at any point. Real-time coordination between workflows.",
},
{
icon: History,
title: "Full History & Replay",
description:
"Complete event history for every workflow execution. Deterministic replay for debugging. Audit trail built in.",
},
{
icon: Lock,
title: "KMS Secrets",
description:
"Inject secrets from Hanzo KMS into workflow activities. Rotated automatically. Never stored in workflow history.",
},
{
icon: Shield,
title: "IAM Integration",
description:
"Hanzo IAM for authentication and authorization. Namespace-level RBAC. Org-scoped data isolation.",
},
{
icon: Users,
title: "Multi-Tenant",
description:
"Namespace isolation per team or customer. Resource quotas. Independent scaling. Shared nothing architecture.",
},
{
icon: Workflow,
title: "Child Workflows",
description:
"Spawn child workflows from parents. Fan-out/fan-in patterns. Hierarchical execution with independent lifecycles.",
},
]
const useCases = [
{
icon: Bot,
title: "AI Agent Orchestration",
description:
"Coordinate multi-step agent workflows with tool calls, human approval gates, and automatic retry on LLM failures. Durable state means agents can run for hours or days.",
},
{
icon: ShoppingCart,
title: "Commerce Sagas",
description:
"Payment capture, inventory reservation, shipping, and notification as a single durable saga. Automatic compensation if any step fails. No lost orders.",
},
{
icon: Workflow,
title: "CI/CD Pipelines",
description:
"Build, test, deploy, and verify as a durable workflow. Retry flaky tests. Wait for manual approval. Roll back on failure. Full execution history.",
},
{
icon: CalendarClock,
title: "Scheduled Jobs",
description:
"Replace fragile cron with durable scheduled workflows. Timezone-aware. Exactly-once execution guarantees. Built-in monitoring and alerting.",
},
{
icon: UserCheck,
title: "Human-in-the-Loop",
description:
"Pause workflows waiting for human input. Approval gates, review steps, and escalation timeouts. Resume exactly where you left off.",
},
]
/** A workflow in one SDK, its code highlighted by the server page. */
export interface Sample {
file: string
delay: number
code: ReactNode
}
export default function TasksPage({ samples }: { samples: Sample[] }) {
return (
<>
{/* Hero Section */}
<XStack
render="section"
position="relative"
minH="90vh"
items="center"
justify="center"
overflow="hidden"
py={80}
px={16}
$sm={{ px: 24 }}
$lg={{ px: 32 }}
>
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1024} mx="auto" $platform-web={{ textAlign: 'center' }}>
<Chip
render={<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} />}
mb={32}
px={16}
py={8}
borderColor="var(--border)"
bg="color-mix(in oklab, var(--primary) 5%, transparent)"
>
<ListChecks width={16} height={16} color="var(--foreground)" />
<Line fontWeight="500" color={SOFT}>
tasks.hanzo.ai
</Line>
</Chip>
<Display
render={<motion.h1 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.1 }} />}
mb={24}
>
Hanzo Tasks
</Display>
<Claim
render={<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.15 }} />}
mb={16}
fontWeight="500"
>
Durable Workflow Execution
</Claim>
<Lede
render={<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} />}
mb={32}
width="100%"
maxW={768}
mx="auto"
>
Build invincible applications. Workflows that survive crashes,
retries that never give up, and sagas that always compensate.
Your code runs to completion or rolls back cleanly. Every time.
</Lede>
<Box
render={<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.25 }} />}
mb={48}
width="100%"
maxW={768}
mx="auto"
>
<Grid columns={STATS} gap={16}>
{heroStats.map((stat) => (
<Box key={stat.label} p={16} {...TILE_BOX}>
<Line render="div" display="block" fontSize="$7" lineHeight="1.3333" fontWeight="700" color="var(--foreground)">
{stat.value}
</Line>
<CardBody render="div">{stat.label}</CardBody>
</Box>
))}
</Grid>
</Box>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<XStack flexWrap="wrap" justify="center" gap={16}>
<Action
href="/login"
fill
{...FILL}
>
Try Hanzo
<ArrowRight width={16} height={16} />
</Action>
</XStack>
</motion.div>
</Box>
</XStack>
{PRODUCT && <AgentPrompt product={PRODUCT} />}
{/* Features Grid */}
<Band pad="$11" measure={1280}>
<Head
title="Workflows That Never Fail"
lede="Every primitive you need for building reliable distributed systems."
/>
<Grid columns={DECK} gap={24}>
{features.map((feature, index) => (
<Leaf
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
key={feature.title}
{...LEAF}
>
<Plate size={48}>
<feature.icon width={24} height={24} color="var(--foreground)" />
</Plate>
<CardTitle>{feature.title}</CardTitle>
<CardBody>{feature.description}</CardBody>
</Leaf>
))}
</Grid>
</Band>
{/* Use Cases */}
<Band pad="$11" measure={1280}>
<Head
title="Built for Real Workloads"
lede="From AI agents to payment sagas, Tasks handles the hard parts."
/>
<Grid columns={DECK} gap={24}>
{useCases.map((useCase, index) => (
<Leaf
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
key={useCase.title}
{...LEAF_SOFT}
>
<Plate size={40}>
<useCase.icon width={20} height={20} color="var(--foreground)" />
</Plate>
<CardTitle>{useCase.title}</CardTitle>
<CardBody>{useCase.description}</CardBody>
</Leaf>
))}
</Grid>
</Band>
{/* SDK Code Examples */}
<Band pad="$11" measure={896}>
<Head
title="Native SDKs for Every Stack"
lede="Write workflows in Go, Python, TypeScript, or Java. Same durable guarantees everywhere."
/>
<Grid columns={DUO} gap={24}>
{samples.map((sample) => (
<Box
key={sample.file}
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: sample.delay }} />}
{...FRAME}
>
<FrameBar file={sample.file} />
{sample.code}
</Box>
))}
</Grid>
</Band>
{/* Architecture */}
<Band pad="$11" measure={896}>
<Head
title="How It Works"
lede="Durable execution powered by event sourcing."
/>
<Box
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: 0.1 }} />}
{...FRAME}
>
<FrameBar file="architecture" />
<Source p={24}>
{architecture}
</Source>
</Box>
</Band>
{/* Pricing */}
<Band pad="$11" measure={1280}>
<Head
title="Simple, Predictable Pricing"
lede="Pay per action. An action is a workflow start, activity execution, signal, or timer fire."
/>
<Grid columns={PLANS} gap={24}>
{pricingTiers.map((tier, index) => (
<YStack
key={tier.name}
render={<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: index * 0.05 }} />}
p={24}
{...(tier.highlight ? LIT : TILE_BOX)}
>
<Box mb={24}>
<CardTitle mb={4}>{tier.name}</CardTitle>
<XStack items="baseline" gap={4}>
<Line fontSize="$8" lineHeight="1.2" fontWeight="700" color="var(--foreground)">
{tier.price}
</Line>
<CardBody render="span" display="inline">
{tier.period}
</CardBody>
</XStack>
<CardBody mt={8}>{tier.description}</CardBody>
</Box>
<YStack gap={12} mb={24} grow={1} shrink={1} flexBasis="0%">
<CardBody render="div" fontWeight="500" color="var(--foreground)">
{tier.actions}
</CardBody>
<CardBody render="div">{tier.namespaces}</CardBody>
<Box borderTopWidth={1} borderColor="var(--border)" pt={12} mt={12}>
{tier.features.map((feature) => (
<XStack key={feature} items="center" gap={8} py={4}>
<Check width={14} height={14} color="color-mix(in oklab, var(--foreground) 60%, transparent)" />
<CardBody render="span" display="inline">
{feature}
</CardBody>
</XStack>
))}
</Box>
</YStack>
<Action
href={tier.ctaHref}
fill={tier.highlight}
justify="center"
px={24}
{...(tier.highlight ? null : { borderColor: 'var(--border)' })}
>
{tier.cta}
</Action>
</YStack>
))}
</Grid>
</Band>
{PRODUCT && <Related product={PRODUCT} />}
<ProductFooter slug="tasks" name="Tasks" />
</>
)
}