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.
129 lines
5.4 KiB
TypeScript
129 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import type { ReactNode } from 'react'
|
|
|
|
/**
|
|
* /playground — the Playground, the catalog product the console serves: every
|
|
* model in every mode, with each run's tokens and cost, then the same request
|
|
* from code. Where to start lives on /learn.
|
|
*
|
|
* NO TAILWIND. The sections under the hero are
|
|
* `components/marketing/band`'s `Band` and `Head`; the type and the controls are
|
|
* @hanzo/ui's marketing components (`ChromeText` for the heading and its badge,
|
|
* `Title`, `Lede`, `Action`, `More`, `Leaf`); the card grids are
|
|
* `@hanzo/ui/grid`'s `Grid`.
|
|
*
|
|
* Every ink, edge and ground is a semantic token of @hanzo/design (`--card`,
|
|
* `--border`, `--muted-foreground`), so both themes draw from one statement.
|
|
* `e2e/gates/playground.spec.ts` measures the page at every width in both.
|
|
*/
|
|
|
|
import React from 'react'
|
|
import { motion } from '@/components/motion'
|
|
import { ArrowRight } from 'lucide-react'
|
|
import ChromeText from '@/components/ui/chrome-text'
|
|
|
|
import { ProductFooter } from "@/components/products/ProductFooter"
|
|
import { Box, Text, 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 { CONSOLE } from '@/components/home/nav-data'
|
|
import { product } from '@/lib/product'
|
|
import { Action, Lede } from '@hanzo/ui/marketing'
|
|
import { mix } from '@hanzo/app/lib/mix'
|
|
import { under } from '@/components/shell/dock'
|
|
|
|
/** 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. The table has no
|
|
* playground command and docs publishes no page for it, so each block is
|
|
* silent on both rather than guessing. */
|
|
const PRODUCT = product('playground')
|
|
|
|
/** The quiet ink. The page's own ink is the layout's `--foreground`. */
|
|
const DIM = 'var(--muted-foreground)'
|
|
|
|
/** A code frame: the same tile, with its label bar clipped by the corner. */
|
|
const FRAME = {
|
|
overflow: 'hidden',
|
|
rounded: 'var(--radius-xl)',
|
|
borderWidth: 1,
|
|
borderColor: 'var(--border)',
|
|
bg: 'var(--card)',
|
|
} as const
|
|
|
|
|
|
/** The pair of controls that closes a section: stacked on a phone, side by side
|
|
* from 640. PROPS, not a style object — an inline `flex-direction` would
|
|
* outrank the atomic class `$sm` emits and the row would never turn. */
|
|
const ROW = { display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 12 } as const
|
|
|
|
|
|
/** A starter: its language, its label, and its code highlighted by the server page. */
|
|
export interface Example {
|
|
lang: string
|
|
label: string
|
|
code: ReactNode
|
|
}
|
|
|
|
const Playground = ({ examples }: { examples: Example[] }) => {
|
|
return (
|
|
<Box minH="100vh">
|
|
{/* $11 and $12, the two rhythms the bands under it keep. */}
|
|
<Box render="section" position="relative" {...under('$11')} pb="$11" $lg={{ pt: under('$12').pt, pb: '$12' }}>
|
|
<Box position="absolute" t={0} r={0} b={0} l={0} background={`linear-gradient(to bottom in oklab, ${mix('var(--pure-white)', 10)}, transparent)`} />
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1280} mx="auto" px={16} $sm={{ px: 24 }} $lg={{ px: 32 }}>
|
|
<Box width="100%" maxW={768} mx="auto">
|
|
{/* The badge is ChromeText's own, which centres the stack it heads. */}
|
|
<ChromeText preHeading="Playground" mb={24}>Try a model, then ship it</ChromeText>
|
|
<Lede mb={32} text="center">
|
|
Chat, completions, decisions, embeddings, image, video, audio and vision, against every
|
|
model in the catalog, with each run's tokens and cost beside the answer.
|
|
</Lede>
|
|
<Box {...ROW} $sm={{ flexDirection: 'row' }}>
|
|
<Action href={`${CONSOLE}/playground`} fill justify="center">
|
|
Open the Playground <ArrowRight width={16} height={16} />
|
|
</Action>
|
|
<Action href="/api" justify="center">
|
|
Read the API
|
|
</Action>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
|
|
{PRODUCT && <AgentPrompt product={PRODUCT} />}
|
|
|
|
<Band rule={false} pad="$11">
|
|
<Head title="Then the same request from code" lede="A key, a model name, and a message is the whole first request." />
|
|
{/* Two starters, two columns: a third track would leave the row half empty. */}
|
|
<Grid columns={{ min: 320, max: 2 }} gap={24}>
|
|
{examples.map((e, i) => (
|
|
<YStack
|
|
key={e.label}
|
|
render={<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.05 }} />}
|
|
{...FRAME}
|
|
>
|
|
<XStack items="center" justify="space-between" px={16} py={8} borderBottomWidth={1} borderColor="var(--border)">
|
|
<Text fontSize="$1" lineHeight="0.9167rem" textTransform="uppercase" letterSpacing={0.55} color={DIM}>
|
|
{e.lang}
|
|
</Text>
|
|
<Text fontSize="$1" lineHeight="0.9167rem" color={mix('var(--foreground)', 70)}>
|
|
{e.label}
|
|
</Text>
|
|
</XStack>
|
|
{e.code}
|
|
</YStack>
|
|
))}
|
|
</Grid>
|
|
</Band>
|
|
|
|
{PRODUCT && <Related product={PRODUCT} />}
|
|
<ProductFooter slug="playground" name="Playground" />
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export default Playground
|