feat: website analysis pipeline, voice agent, configurator improvements
All checks were successful
Build & Push / build-and-push (push) Successful in 6m2s

- 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>
This commit is contained in:
2026-03-28 13:41:35 +01:00
parent 16cd2a74ee
commit bab45b981e
19 changed files with 2923 additions and 119 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useTranslations } from 'next-intl';
import { motion } from 'framer-motion';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '@/lib/utils';
import Button from '@/components/ui/Button';
import Chip from '@/components/ui/Chip';
@@ -100,6 +100,73 @@ export default function StepDetails({ formData, setFormData, onNext, onBack }: S
/>
</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">