Files
zeekayandGitHub 7843241e3f style: the last inline styles are gui props, a kit member or @hanzo/ui (#108)
* style: the last inline styles are gui props, a kit member or @hanzo/ui

Owner rule, 2026-10-22: no style={…}, and no `inline-style:` exception.
hanzo-design.allow.json now allows 0 inline styles (from 36), 0 raw
font sizes (16), 0 raw z-indexes (1) and 42 raw colours (57).

- benchmarks/CodeRace: three lanes of hand-written divs are Lane, Track
  and Dot on gui props; the hues are stated once and each wash and edge
  is mixed from its hue.
- beta: the share card's picture is beta/card's Picture, used by the
  share step and /beta/for/<topic>.
- /contact-sales: the options take @hanzo/ui Picker's `placeholder`
  (8.29.31), which keeps the open list at full ink.
- components/code: each span says `data-code` and @hanzo/ui's new
  styles/code.css inks it from the code theme; the dead Dracula
  legibility pass goes with the per-token style.
- /analytics: the dashboard grows on a CSS view timeline (motion.css
  `grow`) where framer wrote MotionValues per frame. A Band clips with
  `overflow: clip`: `hidden` made it a scroller that never moves, and a
  scroll-driven motion inside it stood half done.
- /buying-specialist: hovers and focus are hoverStyle and focusStyle
  where handlers wrote them onto the element, and a reply's marks are
  elements where they were HTML strings. A reply carries the name from
  the address (`?name=`), which was parsed as HTML.
- ShareWidget's menu stands on --z-dropdown.

LLM.md drops the clause that allowed an `inline-style:` comment.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH

* gates/code: a block's spans say data-code and carry no style of their own

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH

* CodeRace: each part of a row may shrink, as its div did; /contact-sales' Picker inks in $ink

A gui stack keeps its width where a div gave way, so at 390 the target
query and a lane's name pushed the race 136px past the window. The
sizes Picker's choice and list take $ink, as the page drew them.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH

* deps: @hanzo/ui 8.29.35 and @hanzo/design 0.5.39; code samples keep their faces

@hanzo/ui 8.29.35 marks a span's weight and slant as data-face
(codeFace), so a keyword stays bold and a comment italic with no style
on the span; 8.29.34's grow is useScroll's range exactly, and Picker's
`color` is the field's ink. @hanzo/design 0.5.39 passes no
`inline-style:` comment, and the count stays 0. The corner names
8.29.33's Plex Arabic face. The allowance's prose says the numbers it
holds (redeclared-token is 0 since the bar's rungs went).

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH

* gui.css is the one @hanzo/ui 8.29.35 generates ($serif)

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH

* deps: @hanzo/design stays 0.5.35

0.5.38's Arabic faces lead --font-sans with Noto Kufi Arabic, which
font.spec reads as the one binding moving; that bump is the
arabic-faces branch's to make. No inline-style comment is left, so the
zero holds under 0.5.39's rule whenever it lands.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
2026-10-30 06:12:18 -07:00

844 lines
33 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 { Box, Text, View, type GuiElement } from '@hanzo/ui'
import { useEffect, useRef, useState, type FormEvent, type ReactNode } from 'react'
import Link from 'next/link'
import { ArrowLeft, ArrowUp, Sparkles, Users } from 'lucide-react'
import { useIam } from '@hanzo/iam/react'
import { useQuery } from '@/lib/auth/query'
import { phrase } from '@hanzo/i18n'
import { useI18n, type I18n } from '@hanzo/i18n/react'
interface Message {
id: string
role: 'user' | 'assistant'
content: string
quote?: {
plan: string
seats?: number
standardSeats?: number
premiumSeats?: number
monthlyTotal: number
annualTotal: number
savings: number
checkoutUrl?: string
contactSales?: boolean
}
}
/** Symmetrical neural network tree icon matching the specialist branding. */
function SpecialistIcon({ size = 56 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
{/* Central vertical stem */}
<line x1="32" y1="16" x2="32" y2="48" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
{/* Hexagonal branched network */}
<path d="M 32 20 L 20 27 L 20 41 L 32 47 L 44 41 L 44 27 Z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" />
<line x1="32" y1="33" x2="20" y2="27" stroke="currentColor" strokeWidth="2" />
<line x1="32" y1="33" x2="44" y2="27" stroke="currentColor" strokeWidth="2" />
<line x1="32" y1="33" x2="20" y2="41" stroke="currentColor" strokeWidth="2" />
<line x1="32" y1="33" x2="44" y2="41" stroke="currentColor" strokeWidth="2" />
{/* Nodes */}
<circle cx="32" cy="14" r="3.5" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
<circle cx="32" cy="33" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
<circle cx="32" cy="50" r="3.5" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
<circle cx="20" cy="27" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
<circle cx="44" cy="27" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
<circle cx="20" cy="41" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
<circle cx="44" cy="41" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background)" />
</svg>
)
}
/** Formats simple markdown (bold, lists, links) into clean JSX elements. */
function FormattedText({ text }: { text: string }) {
const paragraphs = text.split('\n\n')
return (
<Box display="flex" flexDirection="column" gap={12}>
{paragraphs.map((p, pIdx) => {
const lines = p.split('\n')
const isList = lines.every((l) => l.trim().startsWith('- ') || l.trim().startsWith('* '))
if (isList) {
return (
<View
key={pIdx}
render="ul"
m={0}
pl={20}
display="flex"
flexDirection="column"
gap={6}
>
{lines.map((l, lIdx) => {
const clean = l.replace(/^[-*]\s+/, '')
return <li key={lIdx}>{inline(clean)}</li>
})}
</View>
)
}
return (
<View key={pIdx} render="p" display="block" m={0} $platform-web={{ lineHeight: "1.6" }}>
{inline(p)}
</View>
)
})}
</Box>
)
}
/** Code inside a line of a reply. */
function Snippet({ children }: { children: ReactNode }) {
return (
<Text render="code" bg="var(--white-08)" px={6} py={2} rounded={4} fontFamily="$mono" $platform-web={{ fontSize: '0.9em' }}>
{children}
</Text>
)
}
/**
* The marks a reply uses inside a line — **bold**, *emphasis*, `code` — as
* elements, never as markup: a reply carries the reader's name, which arrives
* in the address, so it is text and nothing in it is ever parsed as HTML.
*/
function inline(line: string): ReactNode[] {
const out: ReactNode[] = []
let at = 0
for (const m of line.matchAll(/\*\*(.*?)\*\*|\*(.*?)\*|`(.*?)`/g)) {
if (m.index > at) out.push(line.slice(at, m.index))
const [, bold, em, code] = m
out.push(
bold !== undefined ? (
<strong key={m.index}>{bold}</strong>
) : em !== undefined ? (
<em key={m.index}>{em}</em>
) : (
<Snippet key={m.index}>{code}</Snippet>
),
)
at = m.index + m[0].length
}
if (at < line.length) out.push(line.slice(at))
return out
}
export default function BuyingSpecialistPage() {
const { t } = useI18n()
const query = useQuery()
const { user } = useIam()
// Display name from account, query param, or fallback to Zach (matching the prompt screenshot)
const name = (query.get('name') || user?.name || 'Zach').trim()
const initials = user?.name
? user.name.split(' ').map((n) => n[0]).join('').slice(0, 2).toUpperCase()
: user?.email
? user.email.slice(0, 2).toUpperCase()
: 'ZK'
const [input, setInput] = useState('')
const [messages, setMessages] = useState<Message[]>([])
const [isThinking, setIsThinking] = useState(false)
const messagesEndRef = useRef<GuiElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const isChatting = messages.length > 0
useEffect(() => {
(messagesEndRef.current as HTMLElement | null)?.scrollIntoView({ behavior: 'smooth' })
}, [messages, isThinking])
// `source` is the English a suggestion was written in: the replies key off English words.
const handleStart = (initialText?: string, source?: string) => {
const userPrompt = initialText || input.trim() || t('Hi! Can you help me find the right plan for my team?')
const asked = source || initialText || input.trim() || 'Hi! Can you help me find the right plan for my team?'
setInput('')
const userMessage: Message = {
id: String(Date.now()),
role: 'user',
content: userPrompt,
}
setMessages((prev) => [...prev, userMessage])
setIsThinking(true)
setTimeout(() => {
const response = generateAssistantResponse(asked, name, t)
setMessages((prev) => [...prev, response])
setIsThinking(false)
}, 700)
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (!input.trim() && !isChatting) {
handleStart()
return
}
if (!input.trim()) return
const userPrompt = input.trim()
setInput('')
const userMessage: Message = {
id: String(Date.now()),
role: 'user',
content: userPrompt,
}
setMessages((prev) => [...prev, userMessage])
setIsThinking(true)
setTimeout(() => {
const response = generateAssistantResponse(userPrompt, name, t)
setMessages((prev) => [...prev, response])
setIsThinking(false)
}, 800)
}
return (
<Box
minH="100vh"
bg="var(--surface-card)"
display="flex"
flexDirection="column"
$platform-web={{ fontFamily: "var(--font-body, system-ui, -apple-system, sans-serif)", color: "var(--foreground)" }}
>
{/* Top Navbar */}
<View
render="header"
position="sticky"
t={0}
z="var(--z-dock)"
height={56}
borderBottomWidth={1}
borderBottomColor="var(--white-08)"
display="flex"
items="center"
justify="space-between"
py={0}
px={20}
flexDirection="row"
bg="transparent"
$platform-web={{ backdropFilter: "blur(12px)" }}
>
<Text
render={<Link href="/pricing" />}
items="center"
gap={8}
color="var(--muted-foreground)"
hoverStyle={{ color: 'var(--foreground)' }}
textDecorationLine="none"
fontSize="$3"
fontWeight="500"
$platform-web={{ display: "inline-flex", transition: "color 0.15s ease", flexDirection: "row" }}
>
<ArrowLeft size={16} data-i18n-mirror />
{t('Return to Hanzo')}
</Text>
<Text fontSize="$3" fontWeight="600" color="var(--foreground)">
{t('Buying specialist')}
</Text>
<Box
title={name}
width={32}
height={32}
rounded={9999}
bg="var(--art-iris)"
display="flex"
items="center"
justify="center"
flexDirection="row"
$platform-web={{ letterSpacing: "0.5px", color: "var(--foreground)", fontSize: "var(--text-xs)", fontWeight: "700" }}
>
{initials}
</Box>
</View>
{/* Main Body */}
<View
render="main"
flex={1}
display="flex"
flexDirection="column"
items="center"
width="100%"
maxW={680}
my={0}
mx="auto"
pt={isChatting ? 24 : 48}
px={20}
pb={140}
$platform-web={{ boxSizing: "border-box" }}
>
{!isChatting ? (
<Box display="flex" flexDirection="column" items="center" width="100%" gap={24}>
{/* Specialist Icon */}
<Box mt={12} $platform-web={{ color: "var(--neutral-300)" }}>
<SpecialistIcon size={56} />
</Box>
{/* Title & Introduction */}
<Box
display="flex"
flexDirection="column"
gap={12}
$platform-web={{ textAlign: "center" }}
>
<Text
render="h1"
display="block"
fontSize="$8"
fontWeight="600"
color="var(--foreground)"
m={0}
$platform-web={{ lineHeight: "1.25", letterSpacing: "-0.02em" }}
>
{t("Hi {name}, let's find the right plan for your team.", { name })}
</Text>
<Text
render="p"
display="block"
fontSize="$4"
color="var(--muted-foreground)"
m={0}
maxW={560}
$platform-web={{ lineHeight: "1.55" }}
>
{t('I\'m an AI buying specialist, here to help you figure out which plan fits, what it\'ll cost, and how to get your team set up. We can do all of it right here.')}
</Text>
</Box>
{/* Value Proposition List */}
<View
render="ul"
width="100%"
mt={12}
mx={0}
mb={0}
pt={0}
pr={0}
pb={0}
pl={20}
display="flex"
flexDirection="column"
gap={14}
$platform-web={{ lineHeight: "1.55", color: "var(--neutral-300)", fontSize: "var(--text-base)" }}
>
<li>
<Text render="strong" color="var(--foreground)">{t('The fastest path in.')}</Text> {t("You'll have a quote and a plan by the end of this conversation.")}
</li>
<li>
<Text render="strong" color="var(--foreground)">{t("The best price you'll get.")}</Text> {t("I'll check for our latest pricing.")}
</li>
<li>
<Text render="strong" color="var(--foreground)">{t('I already know your account.')}</Text> {t("If you're using Hanzo, I can see your current plan, seats, and usage.")}
</li>
<li>
<Text render="strong" color="var(--foreground)">{t('Free to talk.')}</Text> {t("This conversation doesn't count toward your usage.")}
</li>
<li>
<Text render="strong" color="var(--foreground)">{t('A sales rep if you want one.')}</Text> {t("For complex or large deals, I'll hand you to a teammate with everything you've shared already in context.")}
</li>
<li>
<Text render="strong" color="var(--foreground)">{t('Nothing sensitive needed.')}</Text> {t('I can find the right plan with some basic information. Please keep confidential details out of chat.')}
</li>
</View>
{/* Privacy Box */}
<Box
width="100%"
py={14}
px={18}
rounded={12}
borderWidth={1}
borderColor="var(--white-08)"
bg="var(--white-03)"
mt={8}
$platform-web={{ boxSizing: "border-box", fontSize: "var(--text-sm)", color: "var(--muted-foreground)", textAlign: "left" }}
>
{t('By continuing, you agree to our {link}.', {
link: (
<Text
render={<Link href="/privacy" />}
display="inline"
color="var(--muted-foreground)"
textDecorationLine="underline"
>
{t('Privacy Policy')}
</Text>
),
})}
</Box>
{/* Starter Suggestions */}
<Box width="100%" display="flex" flexWrap="wrap" gap={8} mt={12} flexDirection="row">
{[
phrase('We have a team of 10 developers'),
phrase('Explain Standard vs Premium seats'),
phrase('Looking for dedicated VPC & HIPAA'),
phrase('Compare Team vs Enterprise'),
].map((suggestion) => (
<Text
key={suggestion}
render={<button type="button" onClick={() => handleStart(t(suggestion), suggestion)} />}
bg="var(--white-03)"
borderWidth={1}
borderColor="var(--white-08)"
py={8}
px={14}
rounded={9999}
cursor="pointer"
color="var(--neutral-300)"
fontSize="$2"
hoverStyle={{ bg: 'var(--white-08)', borderColor: 'var(--white-20)', color: 'var(--foreground)' }}
$platform-web={{ transition: "all 0.15s ease", display: "inline-block" }}
>
{t('{label} →', { label: t(suggestion) })}
</Text>
))}
</Box>
</Box>
) : (
/* Conversational Chat View */
<Box width="100%" display="flex" flexDirection="column" gap={20}>
{messages.map((m) => (
<Box
key={m.id}
display="flex"
flexDirection="column"
width="100%"
$platform-web={{ alignItems: m.role === 'user' ? 'flex-end' : 'flex-start' }}
>
{m.role === 'assistant' ? (
<Box display="flex" gap={12} items="flex-start" maxW="100%" flexDirection="row">
<Box
width={32}
height={32}
rounded={9999}
bg="var(--white-08)"
borderWidth={1}
borderColor="var(--white-10)"
display="flex"
items="center"
justify="center"
shrink={0}
mt={2}
flexDirection="row"
$platform-web={{ color: "var(--neutral-300)" }}
>
<Sparkles size={16} />
</Box>
<Box display="flex" flexDirection="column" gap={14} flex={1}>
<Box
bg="var(--white-03)"
borderWidth={1}
borderColor="var(--white-08)"
rounded={16}
py={16}
px={20}
$platform-web={{ lineHeight: "1.6", color: "var(--foreground)", fontSize: "var(--text-lg)" }}
>
<FormattedText text={m.content} />
</Box>
{/* Interactive Quote Card if present */}
{m.quote ? (
<Box
rounded={16}
p={20}
display="flex"
flexDirection="column"
gap={14}
$platform-web={{ borderWidth: "1px", borderStyle: "solid", borderColor: "color-mix(in srgb, var(--art-iris) 35%, transparent)", backgroundColor: "color-mix(in srgb, var(--art-iris) 6%, transparent)" }}
>
<Box
display="flex"
justify="space-between"
items="center"
flexDirection="row"
>
<Box display="flex" items="center" gap={8} flexDirection="row">
<Users size={18} color="var(--art-iris)" />
<Text render="strong" fontSize="$4" color="var(--foreground)">
{t('{plan} Configuration', { plan: m.quote.plan })}
</Text>
</Box>
<Text
fontSize="$1"
fontWeight="700"
py={3}
px={8}
rounded={9999}
color="var(--state-success)"
$platform-web={{ backgroundColor: "color-mix(in srgb, var(--state-success) 20%, transparent)" }}
>
{t('SAVE {percent}% ANNUALLY', { percent: m.quote.savings })}
</Text>
</Box>
<Box
gap={12}
$platform-web={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))" }}
>
<Box
py={12}
px={14}
rounded={12}
bg="var(--white-03)"
borderWidth={1}
borderColor="var(--white-08)"
>
<Box
$platform-web={{ fontSize: "var(--text-xs)", color: "var(--muted-foreground)" }}
>{t('Annual Rate (Recommended)')}</Box>
<Box
mt={4}
$platform-web={{ fontSize: "var(--text-2xl)", fontWeight: "700", color: "var(--foreground)" }}
>
${m.quote.annualTotal / 12}
<Text
fontSize="$1"
fontWeight="400"
color="var(--muted-foreground)"
>{t('/mo')}</Text>
</Box>
<Box
mt={2}
$platform-web={{ fontSize: "var(--text-xs)", color: "var(--muted-foreground)" }}
>
{t('Billed ${amount} annually upfront', { amount: m.quote.annualTotal })}
</Box>
</Box>
<Box
py={12}
px={14}
rounded={12}
bg="var(--white-03)"
borderWidth={1}
borderColor="var(--white-06)"
>
<Box
$platform-web={{ fontSize: "var(--text-xs)", color: "var(--muted-foreground)" }}
>{t('Monthly Rate')}</Box>
<Box
mt={4}
$platform-web={{ fontSize: "var(--text-2xl)", fontWeight: "700", color: "var(--foreground)" }}
>
${m.quote.monthlyTotal}
<Text
fontSize="$1"
fontWeight="400"
color="var(--muted-foreground)"
>{t('/mo')}</Text>
</Box>
<Box
mt={2}
$platform-web={{ fontSize: "var(--text-xs)", color: "var(--muted-foreground)" }}
>
{t('Cancel anytime')}
</Box>
</Box>
</Box>
<Box display="flex" gap={10} mt={4} flexWrap="wrap" flexDirection="row">
{m.quote.checkoutUrl ? (
<Text
render={<Link href={m.quote.checkoutUrl} />}
flex={1}
minW={180}
py={10}
px={18}
rounded={9999}
bg="var(--foreground)"
color="var(--background)"
fontWeight="600"
fontSize="$2"
text="center"
textDecorationLine="none"
items="center"
justify="center"
gap={6}
$platform-web={{ display: "inline-flex", flexDirection: "row" }}
>
{t('Continue with {plan} →', { plan: m.quote.plan })}
</Text>
) : null}
<Text
render={<Link href="/contact-sales" />}
py={10}
px={18}
rounded={9999}
borderWidth={1}
borderColor="var(--white-20)"
color="var(--foreground)"
fontWeight="500"
fontSize="$2"
textDecorationLine="none"
items="center"
gap={6}
$platform-web={{ display: "inline-flex", flexDirection: "row" }}
>
{t('Talk to Sales Team')}
</Text>
</Box>
</Box>
) : null}
</Box>
</Box>
) : (
/* User message */
<Box
maxW="85%"
bg="var(--border)"
rounded={16}
py={12}
px={18}
borderWidth={1}
borderColor="var(--white-08)"
$platform-web={{ lineHeight: "1.5", color: "var(--foreground)", fontSize: "var(--text-lg)" }}
>
{m.content}
</Box>
)}
</Box>
))}
{isThinking ? (
<Box
display="flex"
gap={12}
items="center"
flexDirection="row"
$platform-web={{ color: "var(--muted-foreground)", fontSize: "var(--text-sm)" }}
>
<Box
width={32}
height={32}
rounded={9999}
bg="var(--white-06)"
display="flex"
items="center"
justify="center"
flexDirection="row"
>
<Sparkles size={16} />
</Box>
<span>{t('Finding the best plan for your team...')}</span>
</Box>
) : null}
<View ref={messagesEndRef} />
</Box>
)}
</View>
{/* Fixed Bottom Input Bar */}
<Box
position="fixed"
b={0}
l={0}
r={0}
z="var(--z-dock)"
pt={16}
px={20}
pb={24}
display="flex"
justify="center"
flexDirection="row"
$platform-web={{ backgroundImage: "linear-gradient(to top, var(--surface-card) 70%, color-mix(in srgb, var(--background) 0%, transparent) 100%)" }}
>
<View
render={<form onSubmit={handleSubmit} />}
width="100%"
maxW={680}
position="relative"
display="flex"
items="center"
flexDirection="row"
>
<View
render={<input ref={inputRef} type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder={isChatting ? t('Ask the Buying Specialist a question...') : t('Press enter to get started')} />}
width="100%"
bg="var(--white-06)"
borderWidth={1}
borderColor="var(--white-10)"
focusStyle={{ bg: 'var(--white-08)', borderColor: 'var(--white-30)' }}
rounded={9999}
pt={16}
pr={54}
pb={16}
pl={22}
$platform-web={{ outline: "none", boxSizing: "border-box", transition: "border-color 0.15s ease, background-color 0.15s ease", fontSize: "var(--text-lg)", color: "var(--foreground)", display: "inline-block" }}
/>
<View
render={<button type="submit" aria-label={t('Send message')} />}
position="absolute"
r={8}
width={38}
height={38}
rounded={9999}
bg="var(--foreground)"
borderWidth={0}
cursor="pointer"
display="flex"
items="center"
justify="center"
flexDirection="row"
hoverStyle={{ opacity: 0.85 }}
$platform-web={{ transition: "transform 0.15s ease, opacity 0.15s ease", color: "var(--background)" }}
>
<ArrowUp size={18} strokeWidth={2.5} />
</View>
</View>
</Box>
</Box>
)
}
/** Generates specialized domain responses based on user queries and numbers. */
function generateAssistantResponse(prompt: string, userName: string, t: I18n['t']): Message {
const p = prompt.toLowerCase()
const id = String(Date.now())
// Look for team numbers in prompt
const matchNum = p.match(/\b(\d+)\s*(people|developers?|engineers?|members?|seats?|devs?|users?)?\b/)
const num = matchNum ? parseInt(matchNum[1], 10) : null
// 1. Explicit Enterprise or Compliance inquiry
if (p.includes('enterprise') || p.includes('vpc') || p.includes('hipaa') || p.includes('dedicated') || p.includes('cluster') || p.includes('soc 2') || (num && num > 50)) {
return {
id,
role: 'assistant',
content: [
t('For organizations deploying at scale, **Hanzo Enterprise** is the tailored choice.'),
'',
t('Here is what is included:'),
`- ${t('**Dedicated Infrastructure**: Private GPU clusters, custom fine-tuned weights, and unlimited concurrency.')}`,
`- ${t('**Deployment Options**: Host in your own cloud VPC (AWS, GCP, Azure), on-premise servers, or air-gapped offline environments.')}`,
`- ${t('**Security & Governance**: Centralized SAML SSO, SCIM provisioning, granular RBAC, and immutable audit logs.')}`,
`- ${t('**Contracted SLAs**: 99.99% uptime guarantees and 24/7 dedicated engineering support.')}`,
'',
t('Would you like me to connect you with our solutions engineering team to review architecture and prepare a custom proposal?'),
].join('\n'),
quote: {
plan: 'Enterprise',
monthlyTotal: 0,
annualTotal: 0,
savings: 0,
contactSales: true,
},
}
}
// 2. Specific Team seat inquiry or team count detected
if (num && num > 1) {
const standardAnnual = num * 20 * 12
const standardMonthly = num * 25
const mixedStandard = Math.max(1, Math.round(num * 0.7))
const mixedPremium = num - mixedStandard
const mixedAnnual = (mixedStandard * 20 + mixedPremium * 100) * 12
return {
id,
role: 'assistant',
content: [
t('For a team of **{n} members**, the **Hanzo Team** plan provides the best balance of pooled usage, shared workspaces, and team controls.', { n: num }),
'',
t('You have two seat tiers that you can mix on one single bill:'),
`- ${t('**Standard Seat**: **$20/seat/mo** billed annually ($240 upfront) or $25/mo monthly. Ideal for engineers writing code, using Claude Code / Cursor, and running everyday agentic workflows.')}`,
`- ${t('**Premium Seat**: **$100/seat/mo** billed annually ($1,200 upfront) or $125/mo monthly. Provides **5× more usage**, higher rate limits, and priority inference across every model.')}`,
'',
`### ${t('Your Pricing Options for {n} Seats:', { n: num })}`,
`- ${t('**All Standard ({n} seats)**: **${monthly}/mo** billed annually (${annual}/year), saving ${saving} vs monthly.', { n: num, monthly: num * 20, annual: standardAnnual, saving: (standardMonthly * 12) - standardAnnual })}`,
`- ${t('**Mixed Setup ({standard} Standard + {premium} Premium)**: **${monthly}/mo** billed annually (${annual}/year).', { standard: mixedStandard, premium: mixedPremium, monthly: Math.round(mixedAnnual / 12), annual: mixedAnnual })}`,
'',
t('Both setups include unified billing, team workspaces, SSO, and shared automations.'),
].join('\n'),
quote: {
plan: 'Hanzo Team',
seats: num,
standardSeats: num,
monthlyTotal: standardMonthly,
annualTotal: standardAnnual,
savings: 20,
checkoutUrl: `/pay/cart?plan=team&interval=year&quantity=${num}`,
},
}
}
// 3. Difference between Standard and Premium seats
if (p.includes('standard') || p.includes('premium') || p.includes('difference') || p.includes('seat')) {
return {
id,
role: 'assistant',
content: [
t("Here is the exact breakdown between Hanzo's two seat tiers:"),
'',
`### ${t('1. Standard Seat ($20/mo annual / $25 monthly)')}`,
`- ${t('**Everything in Hanzo Pro**, plus shared company workspaces.')}`,
`- ${t('Access to every model (Enso, Zen, Kai, Sonnet, Opus, GPT-5).')}`,
`- ${t('Hanzo Dev and full CLI tooling (Claude Code, Cursor, Codex acceleration).')}`,
`- ${t('Pooled limits and unified billing across your team.')}`,
'',
`### ${t('2. Premium Seat ($100/mo annual / $125 monthly)')}`,
`- ${t('**5× higher usage allowances** and 5× higher request limits.')}`,
`- ${t('**Priority inference** during peak frontier model congestion.')}`,
`- ${t('Early access to unreleased models and experimental agentic harnesses.')}`,
'',
t('Organizations can **freely mix** Standard and Premium seats on a single team account. For instance, in a 10-person engineering team, you can assign 8 Standard seats and 2 Premium seats for lead researchers.'),
].join('\n'),
}
}
// 4. Comparison between Team and Enterprise
if (p.includes('compare') || p.includes('vs')) {
return {
id,
role: 'assistant',
content: [
t('Here is a side-by-side comparison between **Team** and **Enterprise**:'),
'',
`- ${t('**Hanzo Team** ($20–$100/seat/mo):')}`,
` - ${t('Self-serve signup starting at 2 seats.')}`,
` - ${t('Mix Standard ($20) and Premium ($100) seats.')}`,
` - ${t('Multi-user workspace, shared chat, meetings, and shared storage.')}`,
` - ${t('Centralized credit card or invoicing.')}`,
'',
`- ${t('**Hanzo Enterprise** (Custom pricing):')}`,
` - ${t('For organizations with strict compliance, security, or massive scale requirements.')}`,
` - ${t('Custom VPC deployment (AWS, GCP, Azure, or on-premises).')}`,
` - ${t('Dedicated GPU clusters with guaranteed inference latency SLAs.')}`,
` - ${t('Enterprise SAML SSO, SCIM, and contracted 24/7 technical support.')}`,
'',
t('How many team members are you planning to onboard?'),
].join('\n'),
}
}
// 5. Default welcoming response
return {
id,
role: 'assistant',
content: [
t('Great to meet you, {name}! I can help you configure the right setup for your team.', { name: userName }),
'',
t('To give you the exact numbers and plan recommendation:'),
`1. ${t('**How many people** will be using Hanzo?')}`,
`2. ${t('**What are your primary use cases** (e.g. coding agents, Claude Code acceleration, model routing, or custom APIs)?')}`,
`3. ${t('Do you have specific **compliance requirements** like custom VPC or SSO?')}`,
].join('\n'),
}
}