mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
759 lines
32 KiB
TypeScript
759 lines
32 KiB
TypeScript
'use client'
|
||
|
||
import { Box, Text, View } from '@hanzo/ui'
|
||
import { useEffect, useRef, useState, type FormEvent } 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'
|
||
|
||
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, #111113)" />
|
||
<circle cx="32" cy="33" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background, #111113)" />
|
||
<circle cx="32" cy="50" r="3.5" stroke="currentColor" strokeWidth="2" fill="var(--background, #111113)" />
|
||
|
||
<circle cx="20" cy="27" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background, #111113)" />
|
||
<circle cx="44" cy="27" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background, #111113)" />
|
||
<circle cx="20" cy="41" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background, #111113)" />
|
||
<circle cx="44" cy="41" r="3" stroke="currentColor" strokeWidth="2" fill="var(--background, #111113)" />
|
||
</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} dangerouslySetInnerHTML={{ __html: parseInline(clean) }} />
|
||
})}
|
||
</View>
|
||
)
|
||
}
|
||
|
||
return <View
|
||
key={pIdx}
|
||
render={<p dangerouslySetInnerHTML={{ __html: parseInline(p) }} />}
|
||
display="block"
|
||
m={0}
|
||
$platform-web={{ lineHeight: "1.6" }}
|
||
/>
|
||
})}
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
function parseInline(str: string): string {
|
||
return str
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
.replace(/`(.*?)`/g, '<code style="background:var(--white-08);padding:2px 6px;border-radius:4px;font-family:monospace;font-size:0.9em;">$1</code>')
|
||
}
|
||
|
||
export default function BuyingSpecialistPage() {
|
||
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<HTMLDivElement>(null)
|
||
const inputRef = useRef<HTMLInputElement>(null)
|
||
|
||
const isChatting = messages.length > 0
|
||
|
||
useEffect(() => {
|
||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||
}, [messages, isThinking])
|
||
|
||
const handleStart = (initialText?: string) => {
|
||
const userPrompt = 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(userPrompt, name)
|
||
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)
|
||
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" onMouseEnter={(e) => (e.currentTarget.style.color = 'var(--foreground)')} onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--muted-foreground)')} />}
|
||
items="center"
|
||
gap={8}
|
||
color="var(--muted-foreground)"
|
||
textDecorationLine="none"
|
||
fontSize="$3"
|
||
fontWeight="500"
|
||
$platform-web={{ display: "inline-flex", transition: "color 0.15s ease", flexDirection: "row" }}
|
||
>
|
||
<ArrowLeft size={16} />
|
||
Return to Hanzo
|
||
</Text>
|
||
|
||
<Text fontSize="$3" fontWeight="600" color="var(--foreground)">
|
||
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" }}
|
||
>
|
||
Hi {name}, let's find the right plan for your team.
|
||
</Text>
|
||
<Text
|
||
render="p"
|
||
display="block"
|
||
fontSize="$4"
|
||
color="var(--muted-foreground)"
|
||
m={0}
|
||
maxW={560}
|
||
$platform-web={{ lineHeight: "1.55" }}
|
||
>
|
||
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)">The fastest path in.</Text> You'll have a quote and a plan by the end of this conversation.
|
||
</li>
|
||
<li>
|
||
<Text render="strong" color="var(--foreground)">The best price you'll get.</Text> I'll check for our latest pricing.
|
||
</li>
|
||
<li>
|
||
<Text render="strong" color="var(--foreground)">I already know your account.</Text> If you're using Hanzo, I can see your current plan, seats, and usage.
|
||
</li>
|
||
<li>
|
||
<Text render="strong" color="var(--foreground)">Free to talk.</Text> This conversation doesn't count toward your usage.
|
||
</li>
|
||
<li>
|
||
<Text render="strong" color="var(--foreground)">A sales rep if you want one.</Text> 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)">Nothing sensitive needed.</Text> 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" }}
|
||
>
|
||
By continuing, you agree to our{' '}
|
||
<Text
|
||
render={<Link href="/privacy" />}
|
||
display="inline"
|
||
color="var(--muted-foreground)"
|
||
textDecorationLine="underline"
|
||
>
|
||
Privacy Policy
|
||
</Text>
|
||
.
|
||
</Box>
|
||
|
||
{/* Starter Suggestions */}
|
||
<Box width="100%" display="flex" flexWrap="wrap" gap={8} mt={12} flexDirection="row">
|
||
{[
|
||
'We have a team of 10 developers',
|
||
'Explain Standard vs Premium seats',
|
||
'Looking for dedicated VPC & HIPAA',
|
||
'Compare Team vs Enterprise',
|
||
].map((suggestion) => (
|
||
<View
|
||
key={suggestion}
|
||
render={<button type="button" onClick={() => handleStart(suggestion)} onMouseEnter={(e) => {
|
||
e.currentTarget.style.backgroundColor = 'var(--white-08)'
|
||
e.currentTarget.style.borderColor = 'var(--white-20)'
|
||
e.currentTarget.style.color = 'var(--foreground)'
|
||
}} onMouseLeave={(e) => {
|
||
e.currentTarget.style.backgroundColor = 'var(--white-03)'
|
||
e.currentTarget.style.borderColor = 'var(--white-08)'
|
||
e.currentTarget.style.color = 'var(--neutral-300)'
|
||
}} />}
|
||
bg="var(--white-03)"
|
||
borderWidth={1}
|
||
borderColor="var(--white-08)"
|
||
py={8}
|
||
px={14}
|
||
rounded={9999}
|
||
cursor="pointer"
|
||
$platform-web={{ transition: "all 0.15s ease", color: "var(--neutral-300)", fontSize: "var(--text-sm)", display: "inline-block" }}
|
||
>
|
||
{suggestion} →
|
||
</View>
|
||
))}
|
||
</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)">
|
||
{m.quote.plan} Configuration
|
||
</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)" }}
|
||
>
|
||
SAVE {m.quote.savings}% ANNUALLY
|
||
</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)" }}
|
||
>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)"
|
||
>/mo</Text>
|
||
</Box>
|
||
<Box
|
||
mt={2}
|
||
$platform-web={{ fontSize: "var(--text-xs)", color: "var(--muted-foreground)" }}
|
||
>
|
||
Billed ${m.quote.annualTotal} annually upfront
|
||
</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)" }}
|
||
>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)"
|
||
>/mo</Text>
|
||
</Box>
|
||
<Box
|
||
mt={2}
|
||
$platform-web={{ fontSize: "var(--text-xs)", color: "var(--muted-foreground)" }}
|
||
>
|
||
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" }}
|
||
>
|
||
Continue with {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" }}
|
||
>
|
||
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>Finding the best plan for your team...</span>
|
||
</Box>
|
||
) : null}
|
||
|
||
<div 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 ? 'Ask the Buying Specialist a question...' : 'Press enter to get started'} onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--white-30)'
|
||
e.currentTarget.style.backgroundColor = 'var(--white-08)'
|
||
}} onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--white-10)'
|
||
e.currentTarget.style.backgroundColor = 'var(--white-06)'
|
||
}} />}
|
||
width="100%"
|
||
bg="var(--white-06)"
|
||
borderWidth={1}
|
||
borderColor="var(--white-10)"
|
||
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="Send message" onMouseEnter={(e) => (e.currentTarget.style.opacity = '0.85')} onMouseLeave={(e) => (e.currentTarget.style.opacity = '1')} />}
|
||
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"
|
||
$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): 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: `For organizations deploying at scale, **Hanzo Enterprise** is the tailored choice.\n\nHere is what is included:\n- **Dedicated Infrastructure**: Private GPU clusters, custom fine-tuned weights, and unlimited concurrency.\n- **Deployment Options**: Host in your own cloud VPC (AWS, GCP, Azure), on-premise servers, or air-gapped offline environments.\n- **Security & Governance**: Centralized SAML SSO, SCIM provisioning, granular RBAC, and immutable audit logs.\n- **Contracted SLAs**: 99.99% uptime guarantees and 24/7 dedicated engineering support.\n\nWould you like me to connect you with our solutions engineering team to review architecture and prepare a custom proposal?`,
|
||
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: `For a team of **${num} members**, the **Hanzo Team** plan provides the best balance of pooled usage, shared workspaces, and team controls.\n\nYou have two seat tiers that you can mix on one single bill:\n- **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.\n- **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.\n\n### Your Pricing Options for ${num} Seats:\n- **All Standard (${num} seats)**: **$${num * 20}/mo** billed annually ($${standardAnnual}/year), saving $${(standardMonthly * 12) - standardAnnual} vs monthly.\n- **Mixed Setup (${mixedStandard} Standard + ${mixedPremium} Premium)**: **$${Math.round(mixedAnnual / 12)}/mo** billed annually ($${mixedAnnual}/year).\n\nBoth setups include unified billing, team workspaces, SSO, and shared automations.`,
|
||
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: `Here is the exact breakdown between Hanzo's two seat tiers:\n\n### 1. Standard Seat ($20/mo annual / $25 monthly)\n- **Everything in Hanzo Pro**, plus shared company workspaces.\n- Access to every model (Enso, Zen, Kai, Sonnet, Opus, GPT-5).\n- Hanzo Dev and full CLI tooling (Claude Code, Cursor, Codex acceleration).\n- Pooled limits and unified billing across your team.\n\n### 2. Premium Seat ($100/mo annual / $125 monthly)\n- **5× higher usage allowances** and 5× higher request limits.\n- **Priority inference** during peak frontier model congestion.\n- Early access to unreleased models and experimental agentic harnesses.\n\nOrganizations 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.`,
|
||
}
|
||
}
|
||
|
||
// 4. Comparison between Team and Enterprise
|
||
if (p.includes('compare') || p.includes('vs')) {
|
||
return {
|
||
id,
|
||
role: 'assistant',
|
||
content: `Here is a side-by-side comparison between **Team** and **Enterprise**:\n\n- **Hanzo Team** ($20–$100/seat/mo):\n - Self-serve signup starting at 2 seats.\n - Mix Standard ($20) and Premium ($100) seats.\n - Multi-user workspace, shared chat, meetings, and shared storage.\n - Centralized credit card or invoicing.\n\n- **Hanzo Enterprise** (Custom pricing):\n - For organizations with strict compliance, security, or massive scale requirements.\n - Custom VPC deployment (AWS, GCP, Azure, or on-premises).\n - Dedicated GPU clusters with guaranteed inference latency SLAs.\n - Enterprise SAML SSO, SCIM, and contracted 24/7 technical support.\n\nHow many team members are you planning to onboard?`,
|
||
}
|
||
}
|
||
|
||
// 5. Default welcoming response
|
||
return {
|
||
id,
|
||
role: 'assistant',
|
||
content: `Great to meet you, ${userName}! I can help you configure the right setup for your team.\n\nTo give you the exact numbers and plan recommendation:\n1. **How many people** will be using Hanzo?\n2. **What are your primary use cases** (e.g. coding agents, Claude Code acceleration, model routing, or custom APIs)?\n3. Do you have specific **compliance requirements** like custom VPC or SSO?`,
|
||
}
|
||
}
|