mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
/beta's questions, hints and options are marked with phrase() and drawn with t(), so every answer posted to the leads and beta endpoints, every track() payload and every comparison keeps the English value. The referral's tracking channel is a fixed field instead of the button's label, which is now translated. Plurals are two keys, "Skip the rest" sits at the inline end, and the program cards align to the start. The bot and billing pages translate what they hand ProductLanding, with prices and rates as values; /brand wraps its own words.
246 lines
9.8 KiB
TypeScript
246 lines
9.8 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { motion } from '@/components/motion'
|
||
import { ArrowRight, Bot, Check, Cpu, HardDrive, Monitor, Plus, Minus, Shield, Sparkles, Terminal } from 'lucide-react'
|
||
import { Box, Text, View, XStack, YStack } from '@hanzo/ui'
|
||
import { Action, Chip, Eyebrow, Lede, Title } from '@hanzo/ui/marketing'
|
||
import { phrase } from '@hanzo/i18n'
|
||
import { useI18n } from '@hanzo/i18n/react'
|
||
|
||
const PRICE_PER_BOT = 50
|
||
const PRESETS = [1, 2, 3, 5, 10]
|
||
|
||
const PERKS = [
|
||
{
|
||
icon: Monitor,
|
||
title: phrase('Dedicated Cloud Sandbox'),
|
||
desc: phrase('Each extra bot runs in its own isolated Linux environment with dedicated memory and compute.'),
|
||
},
|
||
{
|
||
icon: Terminal,
|
||
title: phrase('Isolated Browser & Shell'),
|
||
desc: phrase('Independent Chromium browser sessions and private bash terminals with separate logins and cookies.'),
|
||
},
|
||
{
|
||
icon: HardDrive,
|
||
title: phrase('Independent Markdown Memory'),
|
||
desc: phrase('Private workspace memory files (MEMORY.md) so multiple bots never pollute each other’s context.'),
|
||
},
|
||
{
|
||
icon: Shield,
|
||
title: phrase('Parallel Schedules & Cron'),
|
||
desc: phrase('Run background jobs simultaneously across multiple bots without hitting single-bot queues.'),
|
||
},
|
||
]
|
||
|
||
export function AdditionalBotsWidget() {
|
||
const [count, setCount] = useState(1)
|
||
const { t } = useI18n()
|
||
|
||
const additionalCost = count * PRICE_PER_BOT
|
||
const totalBots = 1 + count // 1 included on $100+ plan
|
||
|
||
return (
|
||
<Box id="additional-bots" my={48} width="100%">
|
||
<Box
|
||
overflow="hidden"
|
||
rounded="var(--radius-2xl)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-800)"
|
||
p={24}
|
||
$sm={{ p: 36 }}
|
||
$lg={{ p: 48 }}
|
||
$platform-web={{
|
||
background: 'linear-gradient(180deg, color-mix(in oklab, var(--neutral-900) 80%, transparent) 0%, var(--neutral-950) 100%)',
|
||
}}
|
||
>
|
||
<YStack items="center" justify="center" $platform-web={{ textAlign: 'center' }} maxW={720} mx="auto" mb={36}>
|
||
<XStack items="center" gap={8} mb={12}>
|
||
<Chip px={12} py={4} gap={6} bg="var(--neutral-800)">
|
||
<Sparkles width={14} height={14} color="var(--primary)" />
|
||
{t('Add-On for Max Plans ({floor})', { floor: '$100+/mo' })}
|
||
</Chip>
|
||
</XStack>
|
||
<Title quiet fontSize="$7" lineHeight="2rem" fontWeight="700">
|
||
{t('Need more than one resident bot?')}
|
||
</Title>
|
||
<Lede mt={12} fontSize="$3" color="var(--muted-foreground)">
|
||
{t('Every {floor} monthly plan includes {n} resident cloud bot. Add additional resident bots to your organization for {price}.', {
|
||
floor: '$100+',
|
||
n: 1,
|
||
price: (
|
||
<Text color="var(--foreground)" fontWeight="600">
|
||
{t('{price}/bot/month', { price: `$${PRICE_PER_BOT}` })}
|
||
</Text>
|
||
),
|
||
})}
|
||
</Lede>
|
||
</YStack>
|
||
|
||
<Box maxW={880} mx="auto">
|
||
{/* Calculator Card */}
|
||
<Box
|
||
p={24}
|
||
$sm={{ p: 32 }}
|
||
rounded="var(--radius-xl)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-700)"
|
||
$platform-web={{
|
||
backgroundColor: 'color-mix(in oklab, var(--neutral-900) 90%, black)',
|
||
}}
|
||
mb={32}
|
||
>
|
||
<XStack flexWrap="wrap" items="center" justify="space-between" gap={24}>
|
||
<YStack gap={8} shrink={1} minW={0}>
|
||
<Text fontSize="$2" fontWeight="600" textTransform="uppercase" letterSpacing={1} color="var(--muted-foreground)">
|
||
{t('Additional Resident Bots')}
|
||
</Text>
|
||
<XStack items="center" gap={12}>
|
||
<XStack
|
||
render="button"
|
||
{...{ type: 'button', disabled: count <= 1 }}
|
||
onClick={() => setCount((c) => Math.max(1, c - 1))}
|
||
width={40}
|
||
height={40}
|
||
rounded="var(--radius-lg)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-700)"
|
||
bg="var(--neutral-800)"
|
||
items="center"
|
||
justify="center"
|
||
aria-label={t('Decrease additional bots')}
|
||
$platform-web={{
|
||
color: count <= 1 ? 'var(--neutral-600)' : 'var(--foreground)',
|
||
cursor: count <= 1 ? 'not-allowed' : 'pointer',
|
||
transition: 'all 0.15s ease',
|
||
}}
|
||
>
|
||
<Minus width={18} height={18} />
|
||
</XStack>
|
||
|
||
<Text fontSize="$8" fontWeight="700" color="var(--foreground)" minW={44} text="center">
|
||
+{count}
|
||
</Text>
|
||
|
||
<XStack
|
||
render="button"
|
||
{...{ type: 'button' }}
|
||
onClick={() => setCount((c) => Math.min(50, c + 1))}
|
||
width={40}
|
||
height={40}
|
||
rounded="var(--radius-lg)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-700)"
|
||
bg="var(--neutral-800)"
|
||
items="center"
|
||
justify="center"
|
||
aria-label={t('Increase additional bots')}
|
||
$platform-web={{ color: 'var(--foreground)', cursor: 'pointer', transition: 'all 0.15s ease' }}
|
||
>
|
||
<Plus width={18} height={18} />
|
||
</XStack>
|
||
</XStack>
|
||
|
||
{/* Quick preset chips */}
|
||
<XStack gap={8} mt={8} flexWrap="wrap">
|
||
{PRESETS.map((p) => (
|
||
<Text
|
||
key={p}
|
||
render="button"
|
||
{...{ type: 'button' }}
|
||
onClick={() => setCount(p)}
|
||
px={12}
|
||
py={4}
|
||
rounded="var(--radius-full)"
|
||
fontSize="$1"
|
||
fontWeight={count === p ? '600' : '400'}
|
||
borderWidth={1}
|
||
borderColor={count === p ? 'var(--primary)' : 'var(--neutral-800)'}
|
||
color={count === p ? 'var(--foreground)' : 'var(--muted-foreground)'}
|
||
$platform-web={{
|
||
backgroundColor: count === p ? 'color-mix(in oklab, var(--primary) 20%, transparent)' : 'var(--neutral-900)',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.15s ease',
|
||
}}
|
||
>
|
||
{p === 1 ? t('+{n} bot', { n: p }) : t('+{n} bots', { n: p })}
|
||
</Text>
|
||
))}
|
||
</XStack>
|
||
</YStack>
|
||
|
||
{/* Price Calculation Output */}
|
||
<YStack items="flex-end" $platform-web={{ textAlign: 'right' }} gap={4} minW={220}>
|
||
<Text fontSize="$1" textTransform="uppercase" letterSpacing={1} color="var(--muted-foreground)">
|
||
{t('Add-on monthly cost')}
|
||
</Text>
|
||
<XStack items="baseline" gap={4}>
|
||
<Text fontSize="$9" lineHeight="2.25rem" fontWeight="700" color="var(--foreground)">
|
||
+${additionalCost}
|
||
</Text>
|
||
<Text fontSize="$3" color="var(--muted-foreground)">
|
||
{t('/month')}
|
||
</Text>
|
||
</XStack>
|
||
<Text fontSize="$2" color="var(--text-secondary)" mt={4}>
|
||
{t('Total capacity: {bots} ({base} base + {extra} extra)', {
|
||
bots: (
|
||
<Text fontWeight="600" color="var(--foreground)">
|
||
{t('{n} resident bots', { n: totalBots })}
|
||
</Text>
|
||
),
|
||
base: 1,
|
||
extra: count,
|
||
})}
|
||
</Text>
|
||
</YStack>
|
||
</XStack>
|
||
|
||
{/* Action Bar */}
|
||
<Box mt={24} pt={20} borderTopWidth={1} borderColor="var(--neutral-800)">
|
||
<XStack flexWrap="wrap" items="center" justify="space-between" gap={16}>
|
||
<Text fontSize="$2" color="var(--muted-foreground)" maxW={480}>
|
||
{t("Charges apply to your organization's card monthly. Cancel or adjust extra bot capacity anytime.")}
|
||
</Text>
|
||
<Action href={`/login?next=/bots&addon=additional-bots&qty=${count}`} fill>
|
||
{count === 1 ? t('Add {n} Bot to Plan', { n: count }) : t('Add {n} Bots to Plan', { n: count })}{' '}
|
||
<ArrowRight width={16} height={16} data-i18n-mirror />
|
||
</Action>
|
||
</XStack>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Perks Grid */}
|
||
<XStack flexWrap="wrap" gap={16}>
|
||
{PERKS.map((item) => (
|
||
<Box
|
||
key={item.title}
|
||
flex={1}
|
||
minW={200}
|
||
p={20}
|
||
rounded="var(--radius-lg)"
|
||
borderWidth={1}
|
||
borderColor="var(--neutral-800)"
|
||
$platform-web={{
|
||
backgroundColor: 'color-mix(in oklab, var(--neutral-900) 40%, transparent)',
|
||
}}
|
||
>
|
||
<Box mb={12}>
|
||
<item.icon width={20} height={20} color="var(--text-secondary)" />
|
||
</Box>
|
||
<Text fontSize="$3" fontWeight="600" color="var(--foreground)" mb={4}>
|
||
{t(item.title)}
|
||
</Text>
|
||
<Text fontSize="$2" lineHeight="1.25rem" color="var(--muted-foreground)">
|
||
{t(item.desc)}
|
||
</Text>
|
||
</Box>
|
||
))}
|
||
</XStack>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|