feat(portal): complete masterplan with deterministic budget simulator, RAG assistant, agency cross-tenant view and multi-tenant middleware
This commit is contained in:
@@ -19,6 +19,79 @@ export async function POST(req: NextRequest) {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// 0. Intención: Simulación Determinista "¿Qué pasa si...?"
|
||||
if (userMessage.includes('simul') || userMessage.includes('invierto') || userMessage.includes('pasa si') || userMessage.includes('proyec')) {
|
||||
try {
|
||||
// Extraer importe numérico
|
||||
const matchNumber = userMessage.match(/(\d+[\d\.,]*)/);
|
||||
let amount = 1000;
|
||||
if (matchNumber) {
|
||||
amount = parseFloat(matchNumber[1].replace(/\./g, '').replace(',', '.'));
|
||||
}
|
||||
|
||||
let channel: string | null = null;
|
||||
if (userMessage.includes('meta') || userMessage.includes('facebook') || userMessage.includes('instagram')) channel = 'meta';
|
||||
else if (userMessage.includes('google')) channel = 'google';
|
||||
else if (userMessage.includes('tiktok')) channel = 'tiktok';
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
p_tenant_id: tenantId,
|
||||
p_additional_spend: amount,
|
||||
p_target_channel: channel
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const sim = await res.json();
|
||||
const p10 = sim.conservative_p10 || {};
|
||||
const p50 = sim.expected_p50 || {};
|
||||
const p90 = sim.optimistic_p90 || {};
|
||||
const base = sim.baseline || {};
|
||||
|
||||
const text = `🔮 **Simulación de Inversión Adicional (${amount.toLocaleString('es-ES')} € en ${sim.target_channel.toUpperCase()}):**\n\n` +
|
||||
`*Métricas Base Reales:* CPL ${base.historical_cpl} €, Tasa de Cierre ${base.conversion_rate_pct}%, Ticket Medio ${Number(base.avg_ticket || 0).toLocaleString('es-ES')} €.\n\n` +
|
||||
`- **Escenario Esperado (p50):** +${p50.leads} leads, +${p50.deals} clientes, **+${Number(p50.revenue || 0).toLocaleString('es-ES')} €** (ROAS ${p50.roas}x)\n` +
|
||||
`- **Escenario Conservador (p10):** +${p10.leads} leads, +${p10.deals} clientes, **+${Number(p10.revenue || 0).toLocaleString('es-ES')} €** (ROAS ${p10.roas}x)\n` +
|
||||
`- **Escenario Optimista (p90):** +${p90.leads} leads, +${p90.deals} clientes, **+${Number(p90.revenue || 0).toLocaleString('es-ES')} €** (ROAS ${p90.roas}x)\n\n` +
|
||||
`*Proyección calculada matemáticamente sin alucinaciones con la RPC simulate_budget.*`;
|
||||
return Response.json([{ text }]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in simulate_budget chat:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 0.5 Intención: RAG Documental Híbrido (Documentos / Copys / Informes)
|
||||
if (userMessage.includes('document') || userMessage.includes('copy') || userMessage.includes('informe') || userMessage.includes('brief') || userMessage.includes('contenido') || userMessage.includes('activo')) {
|
||||
try {
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/match_documents`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
p_tenant_id: tenantId,
|
||||
p_query_text: userMessage,
|
||||
p_match_count: 3
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
const matches = await res.json();
|
||||
if (Array.isArray(matches) && matches.length > 0) {
|
||||
const excerpts = matches.map((m: any) =>
|
||||
`📄 **${m.document_title}** (${m.document_type.toUpperCase()}):\n> ${m.chunk_text.replace(/\n/g, '\n> ')}`
|
||||
).join('\n\n');
|
||||
|
||||
const text = `📚 **Fragmentos Recuperados de la Biblioteca de Activos (RAG Híbrido):**\n\n${excerpts}\n\n*Fuente: Búsqueda vectorial y de texto completo en document_chunks.*`;
|
||||
return Response.json([{ text }]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in match_documents chat:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Intención: Objetivos vs Real / Estrategia
|
||||
if (userMessage.includes('objetivo') || userMessage.includes('meta') || userMessage.includes('factura') || userMessage.includes('estrategia')) {
|
||||
try {
|
||||
|
||||
37
src/app/api/simulate/route.ts
Normal file
37
src/app/api/simulate/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://supabase.4ventures.es';
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
|
||||
const tenantId = process.env.DEFAULT_TENANT_ID || '0b3c5fc0-6901-4caf-b26a-b6a0201d8541';
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const spend = parseFloat(body.spend || 1000);
|
||||
const channel = body.channel || 'meta';
|
||||
|
||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: serviceKey,
|
||||
Authorization: `Bearer ${serviceKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
p_tenant_id: tenantId,
|
||||
p_additional_spend: spend,
|
||||
p_target_channel: channel === 'blended' ? null : channel
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return Response.json(data);
|
||||
} else {
|
||||
const err = await res.text();
|
||||
return Response.json({ error: err }, { status: 500 });
|
||||
}
|
||||
} catch (e: any) {
|
||||
return Response.json({ error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user