Files
LetsBeBiz-Site/src/app/(frontend)/api/configure/route.ts

464 lines
20 KiB
TypeScript
Raw Normal View History

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': '13 months',
'3-6months': '36 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 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. 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 isFr = body.locale === 'fr';
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(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
? 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');
const hasInfra = services.includes('infrastructure');
let sections = '';
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 (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}**
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 (23 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 },
);
}
}