Files
LetsBeBiz-Site/src/components/configurator/StepDetails.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

215 lines
7.4 KiB
TypeScript

'use client';
import { useTranslations } from 'next-intl';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils';
import Button from '@/components/ui/Button';
import Chip from '@/components/ui/Chip';
import ProgressBar from './ProgressBar';
import type { StepProps } from './WizardContainer';
// ─── Data ─────────────────────────────────────────────────────────────────────
const INDUSTRY_IDS = ['maritime', 'hospitality', 'technology', 'realestate', 'finance', 'ngo', 'other'] as const;
const TIMELINE_IDS = ['asap', '1-3months', '3-6months', 'exploring'] as const;
// ─── Component ────────────────────────────────────────────────────────────────
export default function StepDetails({ formData, setFormData, onNext, onBack }: StepProps) {
const t = useTranslations('configurator');
const selectIndustry = (id: string) => {
setFormData((prev) => ({
...prev,
industry: prev.industry === id ? null : id,
}));
};
const selectTimeline = (id: string) => {
setFormData((prev) => ({
...prev,
timeline: prev.timeline === id ? null : id,
}));
};
return (
<div className="flex flex-col gap-6">
<ProgressBar currentStep={2} />
<div>
<h3 className="font-serif text-2xl font-semibold tracking-headline text-on-surface">
{t('step2.title')}
</h3>
<p className="mt-1 text-sm text-outline">{t('step2.subtitle')}</p>
</div>
{/* Industry */}
<div className="flex flex-col gap-2.5">
<label className="text-xs font-semibold uppercase tracking-label text-outline">
{t('fields.industry')}
</label>
<div className="flex flex-wrap gap-2">
{INDUSTRY_IDS.map((id, index) => (
<motion.div
key={id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: index * 0.04,
duration: 0.3,
ease: [0.16, 1, 0.3, 1],
}}
>
<Chip
active={formData.industry === id}
onClick={() => selectIndustry(id)}
>
{t(`industries.${id}`)}
</Chip>
</motion.div>
))}
</div>
</div>
{/* Scope / Goals */}
<div className="flex flex-col gap-2">
<label
htmlFor="scope-textarea"
className="text-xs font-semibold uppercase tracking-label text-outline"
>
{t('fields.scope')}
<span className="ml-1.5 normal-case font-normal text-outline/70">
{t('fields.scopeOptional')}
</span>
</label>
<textarea
id="scope-textarea"
value={formData.scope}
onChange={(e) =>
setFormData((prev) => ({ ...prev, scope: e.target.value }))
}
placeholder={t('fields.scopePlaceholder')}
rows={4}
className={cn(
'w-full resize-none rounded-xl border border-outline-variant/60 bg-surface-high',
'px-4 py-3 text-sm text-on-surface placeholder:text-outline/50',
'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary',
'transition-colors duration-200',
'leading-relaxed',
)}
/>
</div>
{/* Current Website URL */}
<div className="flex flex-col gap-2">
<label
htmlFor="current-site-url"
className="text-xs font-semibold uppercase tracking-label text-outline"
>
{t('fields.currentSiteUrl')}
<span className="ml-1.5 normal-case font-normal text-outline/70">
{t('fields.currentSiteUrlOptional')}
</span>
</label>
<input
id="current-site-url"
type="url"
value={formData.currentSiteUrl}
onChange={(e) =>
setFormData((prev) => ({ ...prev, currentSiteUrl: e.target.value }))
}
placeholder={t('fields.currentSiteUrlPlaceholder')}
autoComplete="url"
className={cn(
'w-full rounded-xl border border-outline-variant/60 bg-surface-high',
'px-4 py-3 text-sm text-on-surface placeholder:text-outline/50',
'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary',
'transition-colors duration-200',
)}
/>
</div>
{/* Thoughts on current site (conditional) */}
<AnimatePresence>
{formData.currentSiteUrl.trim().length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="overflow-hidden"
>
<div className="flex flex-col gap-2 pt-1">
<label
htmlFor="current-site-thoughts"
className="text-xs font-semibold uppercase tracking-label text-outline"
>
{t('fields.currentSiteThoughts')}
</label>
<textarea
id="current-site-thoughts"
value={formData.currentSiteThoughts}
onChange={(e) =>
setFormData((prev) => ({ ...prev, currentSiteThoughts: e.target.value }))
}
placeholder={t('fields.currentSiteThoughtsPlaceholder')}
rows={3}
className={cn(
'w-full resize-none rounded-xl border border-outline-variant/60 bg-surface-high',
'px-4 py-3 text-sm text-on-surface placeholder:text-outline/50',
'focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary',
'transition-colors duration-200',
'leading-relaxed',
)}
/>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Timeline */}
<div className="flex flex-col gap-2.5">
<label className="text-xs font-semibold uppercase tracking-label text-outline">
{t('fields.timeline')}
</label>
<div className="flex flex-wrap gap-2">
{TIMELINE_IDS.map((id, index) => (
<motion.div
key={id}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: index * 0.05,
duration: 0.3,
ease: [0.16, 1, 0.3, 1],
}}
>
<Chip
active={formData.timeline === id}
onClick={() => selectTimeline(id)}
>
{t(`timelines.${id}`)}
</Chip>
</motion.div>
))}
</div>
</div>
{/* Navigation */}
<div className="flex gap-3">
<Button variant="ghost" onClick={onBack} className="flex-shrink-0">
{t('back')}
</Button>
<Button
variant="primary"
arrow
onClick={onNext}
className="flex-1"
>
{t('nextStep')}
</Button>
</div>
</div>
);
}