631 lines
28 KiB
TypeScript
631 lines
28 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { sendBriefToClient, sendLeadNotification } from '@/lib/email';
|
||
import { analyzeSite, type SiteAnalysis } from '@/lib/site-analysis';
|
||
|
||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||
|
||
interface ConfigureRequestBody {
|
||
services: string[];
|
||
aiEnabled: boolean;
|
||
aiTypes: string[];
|
||
industry: string | null;
|
||
scope: string;
|
||
timeline: string | null;
|
||
name: string;
|
||
company: string;
|
||
email: string;
|
||
phone: string;
|
||
contactPreference: string;
|
||
currentSiteUrl?: string;
|
||
currentSiteThoughts?: string;
|
||
locale?: string;
|
||
}
|
||
|
||
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
||
|
||
const SERVICE_NAMES: Record<string, string> = {
|
||
web: 'Web Design & Development',
|
||
systems: 'Custom Software',
|
||
infrastructure: 'Private Infrastructure',
|
||
};
|
||
|
||
const INDUSTRY_NAMES: Record<string, string> = {
|
||
maritime: 'Maritime & Yachting',
|
||
hospitality: 'Hospitality',
|
||
technology: 'Technology',
|
||
realestate: 'Real Estate',
|
||
finance: 'Finance',
|
||
ngo: 'NGO & Nonprofit',
|
||
other: 'Other',
|
||
};
|
||
|
||
const TIMELINE_NAMES: Record<string, string> = {
|
||
asap: 'As soon as possible',
|
||
'1-3months': '1–3 months',
|
||
'3-6months': '3–6 months',
|
||
exploring: 'Just exploring',
|
||
};
|
||
|
||
const AI_TYPE_NAMES: Record<string, string> = {
|
||
teammate: 'Internal AI Teammate',
|
||
'customer-facing': 'Customer-Facing AI',
|
||
'data-intelligence': 'Data Intelligence',
|
||
notsure: 'AI Integration (approach TBD)',
|
||
};
|
||
|
||
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';
|
||
const company = body.company.trim() || 'Not specified';
|
||
const aiTypeNames = body.aiEnabled && body.aiTypes.length > 0
|
||
? body.aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ')
|
||
: null;
|
||
|
||
let context = `Client Name: ${body.name}
|
||
Company: ${company}
|
||
Services Requested: ${services}
|
||
Industry: ${industry}
|
||
Timeline: ${timeline}`;
|
||
|
||
if (body.aiEnabled) {
|
||
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, 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');
|
||
return generateFallbackBrief(body);
|
||
}
|
||
|
||
console.log('[configure] Generating AI brief via OpenRouter (deepseek/deepseek-v3.2)...');
|
||
|
||
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.
|
||
|
||
Key facts about LetsBe:
|
||
- Every project is designed and coded from scratch — no templates, no page builders
|
||
- They build custom software (CRMs, management platforms, association systems, etc.)
|
||
- They deploy private infrastructure on dedicated servers that the client fully owns and controls
|
||
- They can layer AI integration into any system they build
|
||
- 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.
|
||
|
||
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 langInstructions: Record<string, string> = {
|
||
fr: '\n\nIMPORTANT: Write the entire brief in French. All headings, body text, and next steps must be in French.',
|
||
it: '\n\nIMPORTANT: Write the entire brief in Italian. All headings, body text, and next steps must be in Italian.',
|
||
es: '\n\nIMPORTANT: Write the entire brief in Spanish. All headings, body text, and next steps must be in Spanish.',
|
||
};
|
||
const langInstruction = langInstructions[body.locale ?? ''] ?? '';
|
||
|
||
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. 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 350-500 words.
|
||
|
||
Client details:
|
||
${context}`;
|
||
|
||
try {
|
||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Authorization: `Bearer ${apiKey}`,
|
||
'HTTP-Referer': process.env.NEXT_PUBLIC_SITE_URL || 'https://letsbe.biz',
|
||
'X-Title': 'LetsBe Project Configurator',
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'deepseek/deepseek-v3.2',
|
||
messages: [
|
||
{ role: 'system', content: systemPrompt + langInstruction },
|
||
{ role: 'user', content: userPrompt },
|
||
],
|
||
max_tokens: 1500,
|
||
temperature: 0.7,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.error(`[configure] OpenRouter API error: ${response.status} ${response.statusText}`);
|
||
return generateFallbackBrief(body);
|
||
}
|
||
|
||
console.log('[configure] AI brief generated successfully');
|
||
|
||
const data = await response.json();
|
||
const content = data.choices?.[0]?.message?.content;
|
||
|
||
if (!content) {
|
||
return generateFallbackBrief(body);
|
||
}
|
||
|
||
return content;
|
||
} catch (error) {
|
||
console.error('[configure] AI brief generation failed:', error);
|
||
return generateFallbackBrief(body);
|
||
}
|
||
|
||
}
|
||
|
||
// ─── Fallback Brief (no API key or API failure) ──────────────────────────────
|
||
|
||
function generateFallbackBrief(body: ConfigureRequestBody): string {
|
||
const { services, aiEnabled, aiTypes, industry, scope, timeline, name, company } = body;
|
||
const locale = body.locale ?? 'en';
|
||
|
||
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': '1–3 mois',
|
||
'3-6months': '3–6 mois',
|
||
exploring: 'en phase d\'exploration',
|
||
};
|
||
|
||
const SERVICE_NAMES_IT: Record<string, string> = {
|
||
web: 'Web Design & Sviluppo',
|
||
systems: 'Software Su Misura',
|
||
infrastructure: 'Infrastruttura Privata',
|
||
};
|
||
|
||
const INDUSTRY_NAMES_IT: Record<string, string> = {
|
||
maritime: 'Marittimo & Nautica', hospitality: 'Ospitalità', technology: 'Tecnologia',
|
||
realestate: 'Immobiliare', finance: 'Finanza', ngo: 'ONG & No-Profit', other: 'Altro',
|
||
};
|
||
|
||
const TIMELINE_NAMES_IT: Record<string, string> = {
|
||
asap: 'il prima possibile', '1-3months': '1–3 mesi', '3-6months': '3–6 mesi', exploring: 'in fase di esplorazione',
|
||
};
|
||
|
||
const SERVICE_NAMES_ES: Record<string, string> = {
|
||
web: 'Diseño & Desarrollo Web',
|
||
systems: 'Software a Medida',
|
||
infrastructure: 'Infraestructura Privada',
|
||
};
|
||
|
||
const INDUSTRY_NAMES_ES: Record<string, string> = {
|
||
maritime: 'Marítimo & Náutico', hospitality: 'Hostelería', technology: 'Tecnología',
|
||
realestate: 'Inmobiliario', finance: 'Finanzas', ngo: 'ONG & Sin Ánimo de Lucro', other: 'Otro',
|
||
};
|
||
|
||
const TIMELINE_NAMES_ES: Record<string, string> = {
|
||
asap: 'lo antes posible', '1-3months': '1–3 meses', '3-6months': '3–6 meses', exploring: 'en fase de exploración',
|
||
};
|
||
|
||
const svcMap: Record<string, Record<string, string>> = { fr: SERVICE_NAMES_FR, it: SERVICE_NAMES_IT, es: SERVICE_NAMES_ES };
|
||
const indMap: Record<string, Record<string, string>> = { fr: INDUSTRY_NAMES_FR, it: INDUSTRY_NAMES_IT, es: INDUSTRY_NAMES_ES };
|
||
const tlMap: Record<string, Record<string, string>> = { fr: TIMELINE_NAMES_FR, it: TIMELINE_NAMES_IT, es: TIMELINE_NAMES_ES };
|
||
const svcNames = svcMap[locale] ?? SERVICE_NAMES;
|
||
const indNames = indMap[locale] ?? INDUSTRY_NAMES;
|
||
const tlNames = tlMap[locale] ?? TIMELINE_NAMES;
|
||
|
||
const serviceNames = services.map((s) => svcNames[s] ?? s);
|
||
|
||
const joiners: Record<string, { and: string; commaAnd: string }> = {
|
||
fr: { and: ' et ', commaAnd: ' et ' },
|
||
it: { and: ' e ', commaAnd: ' e ' },
|
||
es: { and: ' y ', commaAnd: ' y ' },
|
||
};
|
||
const j = joiners[locale] ?? { and: ' and ', commaAnd: ', and ' };
|
||
|
||
const servicesList = serviceNames.length <= 2
|
||
? serviceNames.join(j.and)
|
||
: `${serviceNames.slice(0, -1).join(', ')}${j.commaAnd}${serviceNames[serviceNames.length - 1]}`;
|
||
|
||
const industryFallbacks: Record<string, string> = { fr: 'votre secteur', it: 'il tuo settore', es: 'tu sector' };
|
||
const companyFallbacks: Record<string, string> = { fr: 'votre organisation', it: 'la tua organizzazione', es: 'tu organización' };
|
||
const nameFallbacks: Record<string, string> = { fr: 'bonjour', it: 'ciao', es: 'hola' };
|
||
|
||
const industryLabel = industry ? indNames[industry] ?? industry : (industryFallbacks[locale] ?? 'your industry');
|
||
const displayCompany = company.trim() || (companyFallbacks[locale] ?? 'your organization');
|
||
const displayName = name.split(' ')[0] || (nameFallbacks[locale] ?? 'there');
|
||
|
||
const timelineFallbacks: Record<string, string> = {
|
||
fr: 'un calendrier à convenir', it: 'un calendario da definire', es: 'un calendario a convenir',
|
||
};
|
||
const timelineStr = timeline
|
||
? tlNames[timeline]?.toLowerCase() ?? (timelineFallbacks[locale] ?? 'a timeline to be agreed upon')
|
||
: (timelineFallbacks[locale] ?? 'a timeline to be agreed upon');
|
||
|
||
const hasWeb = services.includes('web');
|
||
const hasSystems = services.includes('systems');
|
||
const hasInfra = services.includes('infrastructure');
|
||
|
||
// Build sections per locale
|
||
const sectionTemplates: Record<string, () => string> = {
|
||
fr: () => {
|
||
let s = '';
|
||
if (hasWeb) {
|
||
s += `\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) {
|
||
s += `\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) {
|
||
s += `\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(', ');
|
||
s += `\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) {
|
||
s += `\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()) {
|
||
s += `\n**Vos Objectifs**\nVous avez partagé : "${scope.trim()}" — nous orienterons nos sessions de découverte autour de ces priorités.\n`;
|
||
}
|
||
return s;
|
||
},
|
||
it: () => {
|
||
let s = '';
|
||
if (hasWeb) {
|
||
s += `\n**Web Design & Sviluppo**\nProgetteremo e svilupperemo un sito web su misura per ${displayCompany} da zero — nessun template, nessun page builder. Moderno, responsive, veloce e ottimizzato per i motori di ricerca fin dal primo giorno.\n`;
|
||
}
|
||
if (hasSystems) {
|
||
s += `\n**Software Su Misura**\nRealizzaremo un sistema progettato su misura per come opera ${displayCompany} — modello dati personalizzato, accesso basato sui ruoli e integrazioni con i tuoi strumenti esistenti.\n`;
|
||
}
|
||
if (hasInfra) {
|
||
s += `\n**Infrastruttura Privata**\nConfigureremo un ambiente server dedicato per ${displayCompany} con email, cloud storage e strumenti aziendali che possiedi e controlli interamente.\n`;
|
||
}
|
||
if (aiEnabled && aiTypes.length > 0) {
|
||
const aiLabels = aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ');
|
||
s += `\n**Integrazione IA**\nIntegreremo ${aiLabels.toLowerCase()} nei tuoi sistemi — in profondità, non in superficie. L'approccio esatto sarà definito durante la fase di scoperta.\n`;
|
||
} else if (aiEnabled) {
|
||
s += `\n**Integrazione IA**\nIntegreremo l'IA nei tuoi sistemi — in profondità, non in superficie. L'approccio esatto sarà definito durante la fase di scoperta.\n`;
|
||
}
|
||
if (scope?.trim()) {
|
||
s += `\n**I Tuoi Obiettivi**\nHai condiviso: "${scope.trim()}" — struttureremo le nostre sessioni di scoperta attorno a queste priorità.\n`;
|
||
}
|
||
return s;
|
||
},
|
||
es: () => {
|
||
let s = '';
|
||
if (hasWeb) {
|
||
s += `\n**Diseño & Desarrollo Web**\nDiseñaremos y desarrollaremos un sitio web a medida para ${displayCompany} desde cero — sin plantillas, sin constructores de páginas. Moderno, responsive, rápido y optimizado para motores de búsqueda desde el primer día.\n`;
|
||
}
|
||
if (hasSystems) {
|
||
s += `\n**Software a Medida**\nDesarrollaremos un sistema diseñado específicamente para cómo opera ${displayCompany} — modelo de datos personalizado, acceso basado en roles e integraciones con tus herramientas existentes.\n`;
|
||
}
|
||
if (hasInfra) {
|
||
s += `\n**Infraestructura Privada**\nConfiguraremos un entorno de servidor dedicado para ${displayCompany} con correo electrónico, almacenamiento en la nube y herramientas empresariales que posees y controlas completamente.\n`;
|
||
}
|
||
if (aiEnabled && aiTypes.length > 0) {
|
||
const aiLabels = aiTypes.map((t) => AI_TYPE_NAMES[t] ?? t).join(', ');
|
||
s += `\n**Integración IA**\nIntegraremos ${aiLabels.toLowerCase()} en tus sistemas — de forma profunda, no superficial. El enfoque exacto se definirá durante la fase de descubrimiento.\n`;
|
||
} else if (aiEnabled) {
|
||
s += `\n**Integración IA**\nIntegraremos la IA en tus sistemas — de forma profunda, no superficial. El enfoque exacto se definirá durante la fase de descubrimiento.\n`;
|
||
}
|
||
if (scope?.trim()) {
|
||
s += `\n**Tus Objetivos**\nCompartiste: "${scope.trim()}" — orientaremos nuestras sesiones de descubrimiento en torno a estas prioridades.\n`;
|
||
}
|
||
return s;
|
||
},
|
||
en: () => {
|
||
let s = '';
|
||
if (hasWeb) {
|
||
s += `\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) {
|
||
s += `\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) {
|
||
s += `\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(', ');
|
||
s += `\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) {
|
||
s += `\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()) {
|
||
s += `\n**Your Goals**\nYou shared: "${scope.trim()}" — we'll frame our discovery sessions around these priorities.\n`;
|
||
}
|
||
return s;
|
||
},
|
||
};
|
||
|
||
const sections = (sectionTemplates[locale] ?? sectionTemplates['en'])();
|
||
|
||
if (locale === 'fr') {
|
||
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 (2–3 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`;
|
||
}
|
||
|
||
if (locale === 'it') {
|
||
return `**Brief Progetto per ${displayCompany}**
|
||
Preparato per: ${name}
|
||
Data: ${new Date().toLocaleDateString('it-IT', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||
|
||
---
|
||
|
||
**Panoramica**
|
||
|
||
Ciao ${displayName}, in base al tuo interesse per ${servicesList} nel settore ${industryLabel}, ecco un brief preliminare per guidare la nostra prima conversazione.
|
||
|
||
Affronteremo questo come un progetto unificato — ogni componente che lavora insieme, interamente di tua proprietà e sotto il tuo controllo.
|
||
${sections}
|
||
**Il Nostro Approccio**
|
||
|
||
Iniziamo con una fase di Scoperta (2–3 sessioni) per comprendere le tue esigenze prima di scrivere una sola riga di codice.
|
||
|
||
**Tempistiche**
|
||
|
||
Consegna prevista: ${timelineStr}. Una roadmap dettagliata seguirà la fase di Scoperta.
|
||
|
||
**Prossimi Passi**
|
||
|
||
1. Prenota una chiamata introduttiva gratuita di 30 minuti
|
||
2. Ti invieremo un documento di scoping dettagliato entro 48 ore
|
||
3. La Scoperta inizia — senza impegno
|
||
|
||
Non vediamo l'ora di costruire qualcosa di straordinario insieme.
|
||
|
||
— Il Team LetsBe`;
|
||
}
|
||
|
||
if (locale === 'es') {
|
||
return `**Brief de Proyecto para ${displayCompany}**
|
||
Preparado para: ${name}
|
||
Fecha: ${new Date().toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||
|
||
---
|
||
|
||
**Resumen**
|
||
|
||
Hola ${displayName}, basándonos en tu interés en ${servicesList} para el sector ${industryLabel}, aquí tienes un brief preliminar para guiar nuestra primera conversación.
|
||
|
||
Abordaremos esto como un proyecto unificado — cada componente trabajando en conjunto, totalmente de tu propiedad y bajo tu control.
|
||
${sections}
|
||
**Nuestro Enfoque**
|
||
|
||
Comenzamos con una fase de Descubrimiento (2–3 sesiones) para comprender tus requisitos antes de escribir cualquier línea de código.
|
||
|
||
**Plazo**
|
||
|
||
Entrega objetivo: ${timelineStr}. Una hoja de ruta detallada seguirá a la fase de Descubrimiento.
|
||
|
||
**Próximos Pasos**
|
||
|
||
1. Reserva una llamada introductoria gratuita de 30 minutos
|
||
2. Te enviaremos un documento de alcance detallado en 48 horas
|
||
3. El Descubrimiento comienza — sin compromiso
|
||
|
||
Con ganas de construir algo extraordinario juntos.
|
||
|
||
— El Equipo LetsBe`;
|
||
}
|
||
|
||
return `**Project Brief for ${displayCompany}**
|
||
Prepared for: ${name}
|
||
Date: ${new Date().toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}
|
||
|
||
---
|
||
|
||
**Overview**
|
||
|
||
Hi ${displayName}, based on your interest in ${servicesList} for the ${industryLabel} sector, here's a preliminary brief to guide our first conversation.
|
||
|
||
We'll approach this as a unified project — every component working together, fully owned and controlled by you.
|
||
${sections}
|
||
**Our Approach**
|
||
|
||
We start with a Discovery phase (2–3 sessions) to understand your requirements before writing any code. This ensures we build exactly what you need.
|
||
|
||
**Timeline**
|
||
|
||
Target delivery: ${timelineStr}. A detailed roadmap will follow the Discovery phase.
|
||
|
||
**Next Steps**
|
||
|
||
1. Book a 30-minute introductory call
|
||
2. We'll share a detailed scope document within 48 hours
|
||
3. Discovery begins — no obligation
|
||
|
||
Looking forward to building something great together.
|
||
|
||
— The LetsBe Team`;
|
||
}
|
||
|
||
// ─── Route Handler ────────────────────────────────────────────────────────────
|
||
|
||
export async function POST(request: NextRequest) {
|
||
try {
|
||
const body = (await request.json()) as ConfigureRequestBody;
|
||
|
||
// Validate required fields
|
||
if (!body.services || body.services.length === 0) {
|
||
return NextResponse.json(
|
||
{ success: false, error: 'At least one service must be selected.' },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
|
||
if (!body.name || body.name.trim().length < 2) {
|
||
return NextResponse.json(
|
||
{ success: false, error: 'A valid name is required.' },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
|
||
if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
|
||
return NextResponse.json(
|
||
{ success: false, error: 'A valid email address is required.' },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
|
||
// 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, siteAnalysis);
|
||
|
||
// Send emails (non-blocking — don't fail the response if email fails)
|
||
const smtpHost = process.env.SMTP_HOST;
|
||
const smtpPass = process.env.SMTP_PASS;
|
||
|
||
if (smtpHost && smtpPass) {
|
||
console.log(`[configure] SMTP configured (host: ${smtpHost}), sending emails to ${body.email} and ${process.env.ADMIN_EMAIL || 'hello@letsbe.biz'}...`);
|
||
|
||
Promise.allSettled([
|
||
sendBriefToClient({
|
||
to: body.email,
|
||
name: body.name,
|
||
company: body.company,
|
||
brief,
|
||
}),
|
||
sendLeadNotification({
|
||
to: process.env.ADMIN_EMAIL || 'hello@letsbe.biz',
|
||
name: body.name,
|
||
company: body.company,
|
||
brief,
|
||
services: body.services,
|
||
email: body.email,
|
||
phone: body.phone || undefined,
|
||
contactPreference: body.contactPreference || undefined,
|
||
}),
|
||
]).then((results) => {
|
||
results.forEach((result, i) => {
|
||
const target = i === 0 ? 'client brief' : 'admin notification';
|
||
if (result.status === 'fulfilled') {
|
||
console.log(`[configure] Email sent successfully: ${target}`);
|
||
} else {
|
||
console.error(`[configure] Email failed: ${target}`, result.reason);
|
||
}
|
||
});
|
||
});
|
||
} else {
|
||
console.log(`[configure] SMTP not configured (SMTP_HOST: ${smtpHost ? 'set' : 'missing'}, SMTP_PASS: ${smtpPass ? 'set' : 'missing'}), skipping emails`);
|
||
}
|
||
|
||
return NextResponse.json({ success: true, brief });
|
||
} catch {
|
||
return NextResponse.json(
|
||
{ success: false, error: 'An unexpected error occurred. Please try again.' },
|
||
{ status: 500 },
|
||
);
|
||
}
|
||
}
|