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,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { sendBriefToClient, sendLeadNotification } from '@/lib/email';
import { analyzeSite, type SiteAnalysis } from '@/lib/site-analysis';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -13,6 +14,11 @@ interface ConfigureRequestBody {
name: string;
company: string;
email: string;
phone: string;
contactPreference: string;
currentSiteUrl?: string;
currentSiteThoughts?: string;
locale?: string;
}
// ─── Formatting helpers ──────────────────────────────────────────────────────
@@ -47,7 +53,7 @@ const AI_TYPE_NAMES: Record<string, string> = {
notsure: 'AI Integration (approach TBD)',
};
function buildContext(body: ConfigureRequestBody): string {
function buildContext(body: ConfigureRequestBody, siteAnalysis: SiteAnalysis | null = null): string {
const services = body.services.map((s) => SERVICE_NAMES[s] ?? s).join(', ');
const industry = body.industry ? INDUSTRY_NAMES[body.industry] ?? body.industry : 'Not specified';
const timeline = body.timeline ? TIMELINE_NAMES[body.timeline] ?? body.timeline : 'Not specified';
@@ -66,16 +72,54 @@ Timeline: ${timeline}`;
context += `\nAI Integration: Yes — ${aiTypeNames ?? 'type to be determined'}`;
}
if (body.phone?.trim()) {
context += `\nPhone: ${body.phone.trim()}`;
}
if (body.contactPreference?.trim()) {
context += `\nPreferred Contact Method: ${body.contactPreference.trim()}`;
}
if (body.scope.trim()) {
context += `\nClient's Goals: "${body.scope.trim()}"`;
}
if (body.currentSiteUrl?.trim()) {
context += `\nCurrent Website: ${body.currentSiteUrl.trim()}`;
}
if (body.currentSiteThoughts?.trim()) {
context += `\nClient's Thoughts on Current Site: "${body.currentSiteThoughts.trim()}"`;
}
if (siteAnalysis && !siteAnalysis.fetchError) {
context += '\n\n--- Current Website Analysis ---';
if (siteAnalysis.techStack) {
const { cms, framework, ecommerce, analytics, hosting } = siteAnalysis.techStack;
if (cms) context += `\nCMS: ${cms}`;
if (framework) context += `\nFront-End Framework: ${framework}`;
if (ecommerce) context += `\nE-Commerce: ${ecommerce}`;
if (analytics.length > 0) context += `\nAnalytics: ${analytics.join(', ')}`;
if (hosting) context += `\nHosting: ${hosting}`;
}
if (siteAnalysis.performance) {
const p = siteAnalysis.performance;
context += `\nPerformance Score (mobile): ${p.score}/100`;
context += `\nCore Web Vitals — FCP: ${Math.round(p.fcp)}ms, LCP: ${Math.round(p.lcp)}ms, CLS: ${p.cls.toFixed(2)}, TBT: ${Math.round(p.tbt)}ms`;
}
if (siteAnalysis.title) context += `\nSite Title: ${siteAnalysis.title}`;
if (siteAnalysis.description) context += `\nMeta Description: ${siteAnalysis.description}`;
if (siteAnalysis.primaryColors.length > 0) context += `\nBrand Colors: ${siteAnalysis.primaryColors.join(', ')}`;
if (siteAnalysis.hasForms) context += '\nHas Contact/Lead Forms: Yes';
} else if (siteAnalysis?.fetchError) {
context += `\nNote: Attempted to analyze ${body.currentSiteUrl} but it was unreachable.`;
}
return context;
}
// ─── AI Brief Generation ─────────────────────────────────────────────────────
async function generateBriefWithAI(body: ConfigureRequestBody): Promise<string> {
async function generateBriefWithAI(body: ConfigureRequestBody, siteAnalysis: SiteAnalysis | null = null): Promise<string> {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
console.log('[configure] OPENROUTER_API_KEY not set, using fallback brief template');
@@ -84,7 +128,8 @@ async function generateBriefWithAI(body: ConfigureRequestBody): Promise<string>
console.log('[configure] Generating AI brief via OpenRouter (deepseek/deepseek-v3.2)...');
const context = buildContext(body);
const context = buildContext(body, siteAnalysis);
console.log('[configure] AI context:\n', context);
const displayName = body.name.split(' ')[0] || body.name;
const systemPrompt = `You are writing a project brief on behalf of LetsBe Solutions, a digital studio that builds custom websites, custom software, and private digital infrastructure. The company is American-founded and serves clients internationally.
@@ -97,18 +142,35 @@ Key facts about LetsBe:
- Small, experienced team with decades of combined experience in design and engineering
- They emphasize data ownership, privacy, and digital sovereignty
Write in a professional but warm tone. Be specific and practical — no empty buzzwords. The brief should feel like it was written by someone who understood the client's needs, not a generic template.`;
Write in a professional but warm tone. Be specific and practical — no empty buzzwords. The brief should feel like it was written by someone who understood the client's needs, not a generic template.
Structure the brief for easy scanning — use short paragraphs, bullet points where appropriate, and clear section headings. Avoid walls of text.
Always reference a 30-minute introductory call (not 60 minutes or 1 hour) when mentioning next steps.
When site analysis data is provided in the context, you MUST include a dedicated **Current Website Analysis** section near the top of the brief (after the introduction, before the proposed solution). This section should:
- State what technology the site currently runs on (CMS, framework, hosting)
- If performance data is available, cite the exact score and what it means practically
- Note any strengths or weaknesses observable from the data (e.g., has forms, missing meta description, no analytics)
- If the client shared thoughts about their current site, acknowledge those specifically
- Explain how the proposed solution addresses each issue found
This section demonstrates that LetsBe has already begun analyzing the client's situation before the first call. Never invent data not present in the context — only reference what the analysis actually returned.`;
const langInstruction = body.locale === 'fr'
? '\n\nIMPORTANT: Write the entire brief in French. All headings, body text, and next steps must be in French.'
: '';
const userPrompt = `Generate a personalized project brief for the following prospect. The brief should:
1. Address the client by first name (${displayName})
2. Acknowledge their specific industry and goals
3. For each service they selected, describe concretely what LetsBe would build and why it matters for their business
4. If AI integration is requested, explain practically what that would look like
5. Propose a clear engagement approach (discovery → strategy → build → launch)
6. Include a timeline note based on their preference
7. End with clear next steps
3. For each service they selected, describe concretely what LetsBe would build. Include 2-3 specific, practical benefits the client would gain (e.g., reduced costs, time saved, better guest experience, competitive advantage).
4. Weave in deep industry context — demonstrate understanding of the client's sector, its challenges, and how the proposed solution addresses real pain points in that industry.
5. If AI integration is requested, explain practically what that would look like
6. Propose a clear engagement approach (discovery → strategy → build → launch). Keep each phase to 1-2 sentences maximum.
7. Include a timeline note based on their preference
8. End with a clear next step: book a free 30-minute introductory call to discuss the brief.
Format the brief using **bold** for section headings and --- for separators. Keep it concise but substantive — around 400-600 words.
Format the brief using **bold** for section headings and --- for separators. Keep it concise but substantive — around 350-500 words.
Client details:
${context}`;
@@ -125,7 +187,7 @@ ${context}`;
body: JSON.stringify({
model: 'deepseek/deepseek-v3.2',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'system', content: systemPrompt + langInstruction },
{ role: 'user', content: userPrompt },
],
max_tokens: 1500,
@@ -159,18 +221,47 @@ ${context}`;
function generateFallbackBrief(body: ConfigureRequestBody): string {
const { services, aiEnabled, aiTypes, industry, scope, timeline, name, company } = body;
const isFr = body.locale === 'fr';
const serviceNames = services.map((s) => SERVICE_NAMES[s] ?? s);
const SERVICE_NAMES_FR: Record<string, string> = {
web: 'Design & Développement Web',
systems: 'Logiciels Sur Mesure',
infrastructure: 'Infrastructure Privée',
};
const INDUSTRY_NAMES_FR: Record<string, string> = {
maritime: 'Maritime & Yachting',
hospitality: 'Hôtellerie',
technology: 'Technologie',
realestate: 'Immobilier',
finance: 'Finance',
ngo: 'ONG & Associatif',
other: 'Autre',
};
const TIMELINE_NAMES_FR: Record<string, string> = {
asap: 'dès que possible',
'1-3months': '13 mois',
'3-6months': '36 mois',
exploring: 'en phase d\'exploration',
};
const svcNames = isFr ? SERVICE_NAMES_FR : SERVICE_NAMES;
const indNames = isFr ? INDUSTRY_NAMES_FR : INDUSTRY_NAMES;
const tlNames = isFr ? TIMELINE_NAMES_FR : TIMELINE_NAMES;
const serviceNames = services.map((s) => svcNames[s] ?? s);
const joiner = isFr ? ' et ' : ' and ';
const servicesList = serviceNames.length <= 2
? serviceNames.join(' and ')
: `${serviceNames.slice(0, -1).join(', ')}, and ${serviceNames[serviceNames.length - 1]}`;
const industryLabel = industry ? INDUSTRY_NAMES[industry] ?? industry : 'your industry';
const displayCompany = company.trim() || 'your organization';
const displayName = name.split(' ')[0] || 'there';
? serviceNames.join(joiner)
: `${serviceNames.slice(0, -1).join(', ')}${isFr ? ' et ' : ', and '}${serviceNames[serviceNames.length - 1]}`;
const industryLabel = industry ? indNames[industry] ?? industry : (isFr ? 'votre secteur' : 'your industry');
const displayCompany = company.trim() || (isFr ? 'votre organisation' : 'your organization');
const displayName = name.split(' ')[0] || (isFr ? 'bonjour' : 'there');
const timelineStr = timeline
? TIMELINE_NAMES[timeline]?.toLowerCase() ?? 'a timeline to be agreed upon'
: 'a timeline to be agreed upon';
? tlNames[timeline]?.toLowerCase() ?? (isFr ? 'un calendrier à convenir' : 'a timeline to be agreed upon')
: (isFr ? 'un calendrier à convenir' : 'a timeline to be agreed upon');
const hasWeb = services.includes('web');
const hasSystems = services.includes('systems');
@@ -178,23 +269,76 @@ function generateFallbackBrief(body: ConfigureRequestBody): string {
let sections = '';
if (hasWeb) {
sections += `\n**Web Design & Development**\nWe'll design and build a custom website for ${displayCompany} from scratch — no templates, no page builders. Modern, responsive, fast, and optimized for search engines from day one.\n`;
if (isFr) {
if (hasWeb) {
sections += `\n**Design & Développement Web**\nNous concevrons et développerons un site web sur mesure pour ${displayCompany} — sans templates, sans constructeurs de pages. Moderne, responsive, rapide et optimisé pour le référencement dès le premier jour.\n`;
}
if (hasSystems) {
sections += `\n**Logiciels Sur Mesure**\nNous développerons un système conçu pour correspondre exactement au fonctionnement de ${displayCompany} — modèle de données personnalisé, accès par rôles et intégrations avec vos outils existants.\n`;
}
if (hasInfra) {
sections += `\n**Infrastructure Privée**\nNous mettrons en place un environnement serveur dédié pour ${displayCompany} avec email, stockage cloud et outils métier que vous possédez et contrôlez entièrement.\n`;
}
if (aiEnabled && aiTypes.length > 0) {
const aiLabels = aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ');
sections += `\n**Intégration IA**\nNous intégrerons ${aiLabels.toLowerCase()} dans vos systèmes — en profondeur, pas en surface. L'approche exacte sera définie lors de la phase de découverte.\n`;
} else if (aiEnabled) {
sections += `\n**Intégration IA**\nNous intégrerons l'IA dans vos systèmes — en profondeur, pas en surface. L'approche exacte sera définie lors de la phase de découverte.\n`;
}
if (scope?.trim()) {
sections += `\n**Vos Objectifs**\nVous avez partagé : "${scope.trim()}" — nous orienterons nos sessions de découverte autour de ces priorités.\n`;
}
} else {
if (hasWeb) {
sections += `\n**Web Design & Development**\nWe'll design and build a custom website for ${displayCompany} from scratch — no templates, no page builders. Modern, responsive, fast, and optimized for search engines from day one.\n`;
}
if (hasSystems) {
sections += `\n**Custom Software**\nWe'll build a purpose-made system tailored to how ${displayCompany} actually operates — custom data model, role-based access, and integrations with your existing tools.\n`;
}
if (hasInfra) {
sections += `\n**Private Infrastructure**\nWe'll set up a dedicated server environment for ${displayCompany} with email, cloud storage, and business tools that you fully own and control.\n`;
}
if (aiEnabled && aiTypes.length > 0) {
const aiLabels = aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ');
sections += `\n**AI Integration**\nWe'll layer ${aiLabels.toLowerCase()} into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`;
} else if (aiEnabled) {
sections += `\n**AI Integration**\nWe'll layer AI integration into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`;
}
if (scope?.trim()) {
sections += `\n**Your Goals**\nYou shared: "${scope.trim()}" — we'll frame our discovery sessions around these priorities.\n`;
}
}
if (hasSystems) {
sections += `\n**Custom Software**\nWe'll build a purpose-made system tailored to how ${displayCompany} actually operates — custom data model, role-based access, and integrations with your existing tools.\n`;
}
if (hasInfra) {
sections += `\n**Private Infrastructure**\nWe'll set up a dedicated server environment for ${displayCompany} with email, cloud storage, and business tools that you fully own and control.\n`;
}
if (aiEnabled && aiTypes.length > 0) {
const aiLabels = aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ');
sections += `\n**AI Integration**\nWe'll layer ${aiLabels.toLowerCase()} into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`;
} else if (aiEnabled) {
sections += `\n**AI Integration**\nWe'll layer AI integration into your systems — deeply integrated, not bolted on. The exact approach will be scoped during discovery.\n`;
}
if (scope?.trim()) {
sections += `\n**Your Goals**\nYou shared: "${scope.trim()}" — we'll frame our discovery sessions around these priorities.\n`;
if (isFr) {
return `**Brief Projet pour ${displayCompany}**
Préparé pour : ${name}
Date : ${new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}
---
**Aperçu**
Bonjour ${displayName}, suite à votre intérêt pour ${servicesList} dans le secteur ${industryLabel}, voici un brief préliminaire pour guider notre première conversation.
Nous aborderons ceci comme un projet unifié — chaque composant fonctionnant ensemble, entièrement détenu et contrôlé par vous.
${sections}
**Notre Approche**
Nous commençons par une phase de Découverte (23 sessions) pour comprendre vos besoins avant d'écrire la moindre ligne de code.
**Calendrier**
Livraison cible : ${timelineStr}. Une feuille de route détaillée suivra la phase de Découverte.
**Prochaines Étapes**
1. Réservez un appel de présentation de 30 minutes
2. Nous vous enverrons un document de cadrage détaillé sous 48 heures
3. La Découverte commence — sans engagement
Au plaisir de construire quelque chose de formidable ensemble.
— L'équipe LetsBe`;
}
return `**Project Brief for ${displayCompany}**
@@ -256,8 +400,20 @@ export async function POST(request: NextRequest) {
);
}
// Analyze current website if URL provided
let siteAnalysis: SiteAnalysis | null = null;
if (body.currentSiteUrl?.trim()) {
console.log(`[configure] Analyzing site: ${body.currentSiteUrl.trim()}...`);
siteAnalysis = await analyzeSite(body.currentSiteUrl.trim());
console.log(`[configure] Site analysis complete (fetchError: ${siteAnalysis.fetchError ?? 'none'})`);
console.log(`[configure] Tech stack:`, JSON.stringify(siteAnalysis.techStack));
console.log(`[configure] Performance:`, JSON.stringify(siteAnalysis.performance));
console.log(`[configure] Colors:`, siteAnalysis.primaryColors);
console.log(`[configure] Title:`, siteAnalysis.title);
}
// Generate the brief (AI if available, fallback otherwise)
const brief = await generateBriefWithAI(body);
const brief = await generateBriefWithAI(body, siteAnalysis);
// Send emails (non-blocking — don't fail the response if email fails)
const smtpHost = process.env.SMTP_HOST;
@@ -280,6 +436,8 @@ export async function POST(request: NextRequest) {
brief,
services: body.services,
email: body.email,
phone: body.phone || undefined,
contactPreference: body.contactPreference || undefined,
}),
]).then((results) => {
results.forEach((result, i) => {