Files
hanzo.ai/components/bot/AdditionalBotsWidget.tsx
zeekay 713e9c815a beta, bot, billing, brand: the words go through @hanzo/i18n
/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.
2026-10-22 18:13:04 -07:00

246 lines
9.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}