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

@@ -0,0 +1,73 @@
'use client';
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { Keyboard, Mic } from 'lucide-react';
import { cn } from '@/lib/utils';
// ─── Types ───────────────────────────────────────────────────────────────────
interface ModeToggleProps {
mode: 'type' | 'talk';
onChange: (mode: 'type' | 'talk') => void;
}
// ─── Component ───────────────────────────────────────────────────────────────
export default function ModeToggle({ mode, onChange }: ModeToggleProps) {
const t = useTranslations('configurator');
const [voiceSupported, setVoiceSupported] = useState(false);
useEffect(() => {
async function check() {
if (typeof WebSocket === 'undefined') return;
if (!navigator.mediaDevices?.getUserMedia) return;
try {
const res = await fetch('/api/gemini-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ locale: 'en' }),
});
const data = (await res.json()) as { success: boolean };
if (data.success) setVoiceSupported(true);
} catch {
// silent — toggle stays hidden
}
}
void check();
}, []);
if (!voiceSupported) return null;
return (
<div className="flex items-center gap-1 rounded-xl bg-surface-low p-1 border border-outline-variant/30">
<button
type="button"
onClick={() => onChange('type')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200',
mode === 'type'
? 'bg-white text-on-surface shadow-card'
: 'text-outline hover:text-on-surface',
)}
>
<Keyboard size={13} />
{t('mode.type')}
</button>
<button
type="button"
onClick={() => onChange('talk')}
className={cn(
'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200',
mode === 'talk'
? 'bg-white text-on-surface shadow-card'
: 'text-outline hover:text-on-surface',
)}
>
<Mic size={13} />
{t('mode.talk')}
</button>
</div>
);
}