Files
LetsBeBiz-Site/src/components/configurator/StepComplete.tsx
Matt bab45b981e
All checks were successful
Build & Push / build-and-push (push) Successful in 6m2s
feat: website analysis pipeline, voice agent, configurator improvements
- Site analysis: cheerio HTML parsing, inline tech stack detection (~20 CMS/framework/analytics signatures), Google PageSpeed API integration
- Gemini Live voice agent: WebSocket-based real-time voice mode with live transcript, selection chips, and mid-conversation website analysis
- Type/Talk mode toggle with silent capability detection
- Stepped progress animation during brief generation (4 animated steps)
- URL + thoughts fields in Step 2, phone + contact preference in Step 3
- AI prompt improvements: dedicated website analysis section, 30-min call, concrete benefits, industry depth
- Email redesign: branded templates with logo, proper markdown rendering for both client and admin
- French locale support for AI-generated briefs
- Smaller checkmark, compact booking CTA, expanded brief area

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 13:41:35 +01:00

176 lines
5.8 KiB
TypeScript

'use client';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { Calendar, RotateCcw } from 'lucide-react';
import AnimatedCheckmark from '@/components/icons/AnimatedCheckmark';
import Button from '@/components/ui/Button';
import CalButton from '@/components/ui/CalButton';
import type { WizardFormData } from './WizardContainer';
// ─── Brief Renderer ──────────────────────────────────────────────────────────
function renderBrief(brief: string) {
const blocks = brief.split('\n\n').filter(Boolean);
return blocks.map((block, blockIdx) => {
const lines = block.split('\n').filter(Boolean);
const isSectionHeading =
lines.length === 1 && lines[0].startsWith('**') && lines[0].endsWith('**');
if (isSectionHeading) {
const text = lines[0].replace(/\*\*/g, '');
if (blockIdx === 0) {
return (
<p key={blockIdx} className="font-semibold text-sm text-on-surface mb-0.5">
{text}
</p>
);
}
return (
<p key={blockIdx} className="font-semibold text-xs text-primary-dark uppercase tracking-label mt-4 mb-1">
{text}
</p>
);
}
if (lines.length === 1 && lines[0] === '---') {
return <hr key={blockIdx} className="border-outline-variant/30 my-3" />;
}
return (
<div key={blockIdx} className="text-xs text-outline leading-relaxed">
{lines.map((line, lineIdx) => {
const parts = line.split(/(\*\*[^*]+\*\*)/g);
return (
<p key={lineIdx} className={lineIdx > 0 ? 'mt-1' : ''}>
{parts.map((part, pIdx) =>
part.startsWith('**') && part.endsWith('**') ? (
<strong key={pIdx} className="font-semibold text-on-surface">
{part.slice(2, -2)}
</strong>
) : (
<span key={pIdx}>{part}</span>
),
)}
</p>
);
})}
</div>
);
});
}
// ─── Main Component ──────────────────────────────────────────────────────────
interface StepCompleteProps {
formData: WizardFormData;
brief: string;
onReset?: () => void;
}
export default function StepComplete({ formData, brief, onReset }: StepCompleteProps) {
const t = useTranslations('configurator');
const displayEmail = formData.email || 'your inbox';
const containerVariants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.12 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] as const },
},
};
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="flex flex-col gap-4"
>
{/* Checkmark + heading */}
<motion.div variants={itemVariants} className="flex flex-col items-center text-center pt-1 pb-0">
<AnimatedCheckmark size={40} color="#006494" />
<h3 className="font-serif text-xl font-semibold tracking-headline text-on-surface mt-2.5">
{t('complete.title')}
</h3>
<p className="text-sm text-outline mt-2">
{t('complete.subtitle', { email: displayEmail })}
</p>
</motion.div>
{/* Brief preview */}
{brief && (
<motion.div
variants={itemVariants}
className="rounded-xl bg-surface-high border border-outline-variant/40 px-5 py-5 shadow-card"
>
<p className="text-xs font-semibold uppercase tracking-label text-outline mb-3">
{t('complete.briefPreview')}
</p>
<div className="space-y-1 max-h-[28rem] overflow-y-auto pr-1 scrollbar-thin">
{renderBrief(brief)}
</div>
</motion.div>
)}
{/* Next step: book a call */}
<motion.div variants={itemVariants}>
<div className="flex items-center justify-between gap-4 rounded-lg border border-primary/20 bg-primary/5 px-4 py-3">
<div className="min-w-0">
<p className="text-sm font-semibold text-on-surface">
{t('complete.nextStep')}
</p>
<p className="text-xs text-outline mt-0.5">
{t('complete.bookSubtitle')}
</p>
</div>
<CalButton
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white whitespace-nowrap transition-all hover:-translate-y-px active:translate-y-0 flex-shrink-0"
style={{ background: 'linear-gradient(135deg, #006494, #5BA4D9)' }}
>
<Calendar size={14} />
{t('complete.bookCall')}
</CalButton>
</div>
</motion.div>
{/* Fallback contact + reset */}
<motion.div variants={itemVariants} className="flex flex-col gap-3">
<p className="text-center text-xs text-outline">
{t('complete.reachDirectly')}{' '}
<a
href="mailto:hello@letsbe.biz"
className="text-primary-dark underline underline-offset-2 hover:text-primary transition-colors"
>
hello@letsbe.biz
</a>
</p>
{onReset && (
<button
type="button"
onClick={onReset}
className="flex items-center justify-center gap-1.5 text-xs text-outline hover:text-on-surface transition-colors mx-auto"
>
<RotateCcw size={12} />
{t('startOver')}
</button>
)}
</motion.div>
</motion.div>
);
}