diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index f02a311..67a4e4c 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,16 +1,43 @@ -import { NextRequest } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/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'; + // 1. Extraer mensaje del usuario con compatibilidad para cualquier estructura de body let userMessage = ''; try { const body = await req.json(); - userMessage = (body.message || body.prompt || '').toLowerCase(); - } catch { - // empty body fallback + if (typeof body.message === 'string' && body.message.trim()) { + userMessage = body.message.trim(); + } else if (typeof body.prompt === 'string' && body.prompt.trim()) { + userMessage = body.prompt.trim(); + } else if (Array.isArray(body.messages) && body.messages.length > 0) { + const userMsgs = body.messages.filter((m: any) => m.role === 'user'); + if (userMsgs.length > 0) { + userMessage = userMsgs[userMsgs.length - 1].content || ''; + } + } + } catch (e) { + console.error('Error parsing chat body:', e); + } + + const query = userMessage.toLowerCase().trim(); + + // 2. Obtener tenant_id dinámico del usuario autenticado + let tenantId = '0b3c5fc0-6901-4caf-b26a-b6a0201d8541'; + let companyName = '4Ventures Central'; + + try { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (user) { + tenantId = user.app_metadata?.tenant_id || user.user_metadata?.tenant_id || tenantId; + companyName = user.user_metadata?.company || companyName; + } + } catch (e) { + console.error('Error resolving user tenant in chat:', e); } const headers = { @@ -19,20 +46,57 @@ 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')) { + // Helper para devolver respuesta uniforme + const reply = (text: string) => { + return NextResponse.json([{ text }]); + }; + + // 3. Intención: Saludos y Presentación + if ( + query === '' || + query === 'hola' || + query === 'buenas' || + query === 'buenos dias' || + query === 'buenas tardes' || + query === 'hey' || + query.includes('que puedes hacer') || + query.includes('quien eres') || + query.includes('ayuda') + ) { + return reply( + `👋 **¡Hola! Soy el Asistente Financiero y de Crecimiento de 4Ventures** para **${companyName}**.\n\n` + + `Estoy conectado directamente a tu base de datos analítica y ejecuto cálculos matemáticos deterministas (temperatura 0.0, sin alucinaciones):\n\n` + + `💡 **¿Qué puedes consultarme?**\n` + + `1. **Simulaciones presupuestarias:** *«¿Qué pasa si invierto 1.500 € en Meta?»* o *«Simular 2.000 € en Google»*.\n` + + `2. **Cumplimiento de Objetivos:** *«¿Cómo vamos con los objetivos de este mes?»*.\n` + + `3. **Embudo y Conversión:** *«Muéstrame el embudo comercial y tasa de cierre»*.\n` + + `4. **Métricas de CAC y ROAS:** *«¿Cuál es el CAC y ROAS de los últimos 30 días?»*.\n` + + `5. **Ranking de Anuncios:** *«¿Cuáles son las mejores campañas activas?»*.\n` + + `6. **Biblioteca de Activos:** *«Busca copys de Meta Ads o informes de auditoría»*.\n\n` + + `¿Qué métrica o escenario te gustaría analizar hoy?` + ); + } + + // 4. Intención: Simulación Determinista "¿Qué pasa si...?" + if ( + query.includes('simul') || + query.includes('invierto') || + query.includes('pasa si') || + query.includes('proyec') || + query.includes('presupuesto') + ) { try { - // Extraer importe numérico - const matchNumber = userMessage.match(/(\d+[\d\.,]*)/); + const matchNumber = query.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'; + if (query.includes('meta') || query.includes('facebook') || query.includes('instagram')) channel = 'meta'; + else if (query.includes('google')) channel = 'google'; + else if (query.includes('tiktok')) channel = 'tiktok'; + else if (query.includes('linkedin')) channel = 'linkedin'; const res = await fetch(`${supabaseUrl}/rest/v1/rpc/simulate_budget`, { method: 'POST', @@ -51,21 +115,143 @@ export async function POST(req: NextRequest) { 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 }]); + const channelLabel = (sim.target_channel || 'General').toUpperCase(); + const text = + `🔮 **Simulación de Inversión Adicional (+${amount.toLocaleString('es-ES')} € en ${channelLabel}):**\n\n` + + `📈 **Métricas Base Observadas:**\n` + + `- **CPL Histórico:** ${Number(base.historical_cpl || 0).toFixed(2)} €\n` + + `- **Tasa de Cierre:** ${Number(base.conversion_rate_pct || 0).toFixed(1)}%\n` + + `- **Ticket Medio:** ${Number(base.avg_ticket || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €\n\n` + + `📊 **Proyección por Escenarios Matemáticos:**\n` + + `- **Escenario Esperado (p50):** +${p50.leads} leads | +${p50.deals} clientes | **+${Number(p50.revenue || 0).toLocaleString('es-ES')} €** facturación (ROAS **${p50.roas}x**)\n` + + `- **Escenario Conservador (p10):** +${p10.leads} leads | +${p10.deals} clientes | **+${Number(p10.revenue || 0).toLocaleString('es-ES')} €** facturación (ROAS **${p10.roas}x**)\n` + + `- **Escenario Optimista (p90):** +${p90.leads} leads | +${p90.deals} clientes | **+${Number(p90.revenue || 0).toLocaleString('es-ES')} €** facturación (ROAS **${p90.roas}x**)\n\n` + + `💡 *Cálculo determinista con la función RPC simulate_budget basada en la atribución de cohorte a 90 días.*`; + + return reply(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')) { + // 5. Intención: Objetivos vs Real / Estrategia + if ( + query.includes('objetivo') || + query.includes('meta') || + query.includes('factura') || + query.includes('target') || + query.includes('cumplimiento') + ) { + try { + const now = new Date(); + const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`; + const res = await fetch(`${supabaseUrl}/rest/v1/rpc/targets_vs_actual`, { + method: 'POST', + headers, + body: JSON.stringify({ p_month: currentMonth }) + }); + + if (res.ok) { + const data = await res.json(); + const t = data[0] || {}; + const revAct = Number(t.revenue_actual || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 }); + const revTar = Number(t.revenue_target || 0).toLocaleString('es-ES'); + const pct = t.revenue_achievement_pct || 0; + + const text = + `🎯 **Cumplimiento de Objetivos del Mes (${currentMonth}):**\n\n` + + `- **Facturación:** **${revAct} €** logrados de una meta de **${revTar} €** (${pct}% completado).\n` + + `- **Leads Captados:** **${t.leads_actual || 0}** / ${t.leads_target || 0} objetivo.\n` + + `- **Clientes Ganados:** **${t.clients_actual || 0}** / ${t.clients_target || 0} objetivo.\n\n` + + `📌 **Diagnóstico:** ${pct >= 100 ? '🎉 ¡Objetivo de facturación alcanzado!' : pct >= 50 ? '⚡ Buen ritmo de avance en el periodo.' : '⚠️ Requiere acelerar el volumen de leads calificados o mejorar tasa de contacto.'}\n\n` + + `*Fuente: RPC targets_vs_actual sincronizado con deals ganados en CRM.*`; + + return reply(text); + } + } catch (e) { + console.error('Error in targets_vs_actual:', e); + } + } + + // 6. Intención: Embudo / Funnel / Conversión + if ( + query.includes('funnel') || + query.includes('embudo') || + query.includes('tasa') || + query.includes('ticket') || + query.includes('conversion') || + query.includes('cierre') + ) { + try { + const res = await fetch(`${supabaseUrl}/rest/v1/rpc/lead_funnel`, { + method: 'POST', + headers, + body: JSON.stringify({ p_tenant_id: tenantId, p_days: 30 }) + }); + + if (res.ok) { + const f = await res.json(); + const text = + `🔄 **Embudo de Conversión Comercial (Últimos 30 días):**\n\n` + + `1. **Leads Inbound Captados:** ${f.total_leads || 0}\n` + + `2. **Leads Contactados:** ${f.contacted_leads || 0}\n` + + `3. **Oportunidades en Pipeline:** ${f.deals_in_pipeline || 0}\n` + + `4. **Ventas Cerradas Ganadas:** **${f.deals_won || 0}**\n\n` + + `💰 **Resultados Financieros:**\n` + + `- **Facturación Cerrada:** **${Number(f.revenue_won || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €**\n` + + `- **Ticket Medio:** **${Number(f.avg_ticket || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €**\n` + + `- **Tasa de Cierre Lead ➔ Venta:** **${f.conversion_lead_to_deal_pct || 0}%**\n\n` + + `*Fuente: Webhook deal-won n8n y tabla deals en PostgreSQL.*`; + + return reply(text); + } + } catch (e) { + console.error('Error in lead_funnel:', e); + } + } + + // 7. Intención: Ranking de Campañas / Anuncios + if ( + query.includes('campa') || + query.includes('anuncio') || + query.includes('ranking') || + query.includes('mejor') || + query.includes('ads') + ) { + try { + const res = await fetch(`${supabaseUrl}/rest/v1/rpc/rank_campaigns`, { + method: 'POST', + headers, + body: JSON.stringify({ p_tenant_id: tenantId, p_metric: 'spend', p_limit: 5 }) + }); + + if (res.ok) { + const campaigns = await res.json(); + if (Array.isArray(campaigns) && campaigns.length > 0) { + const list = campaigns.map((c: any, i: number) => + `${i + 1}. **${c.campaign_name}** (${c.platform.toUpperCase()}):\n - Inversión: **${Number(c.spend || 0).toFixed(2)} €**\n - Clics: ${c.clicks || 0} (CTR: ${c.impressions > 0 ? ((c.clicks / c.impressions) * 100).toFixed(2) : 0}%)\n - Leads Captados: **${c.leads || 0}**` + ).join('\n\n'); + + return reply(`🏆 **Top Campañas por Inversión Publicitaria:**\n\n${list}\n\n*Fuente: Datos diarios procesados vía Windsor.ai.*`); + } else { + return reply('📢 Actualmente no constan campañas activas con inversión registrada en los últimos 30 días para este portal.'); + } + } + } catch (e) { + console.error('Error in rank_campaigns:', e); + } + } + + // 8. Intención: RAG Documental Híbrido (Biblioteca de Activos) + if ( + query.includes('document') || + query.includes('copy') || + query.includes('informe') || + query.includes('brief') || + query.includes('activo') || + query.includes('creativo') + ) { try { const res = await fetch(`${supabaseUrl}/rest/v1/rpc/match_documents`, { method: 'POST', @@ -76,6 +262,7 @@ export async function POST(req: NextRequest) { p_match_count: 3 }) }); + if (res.ok) { const matches = await res.json(); if (Array.isArray(matches) && matches.length > 0) { @@ -83,8 +270,17 @@ export async function POST(req: NextRequest) { `📄 **${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 }]); + return reply(`📚 **Fragmentos Recuperados de la Biblioteca de Activos (RAG Híbrido):**\n\n${excerpts}\n\n*Búsqueda híbrida vectorial (pgvector) y BM25 en document_chunks.*`); + } + } + + // Si no hay chunks en RAG, buscar documentos aprobados directamente + const docRes = await fetch(`${supabaseUrl}/rest/v1/documents?tenant_id=eq.${tenantId}&status=eq.approved&select=id,title,type,content`, { headers }); + if (docRes.ok) { + const docs = await docRes.json(); + if (Array.isArray(docs) && docs.length > 0) { + const docList = docs.map((d: any) => `- **${d.title}** (${d.type.toUpperCase()})`).join('\n'); + return reply(`📁 **Documentos Aprobados Disponibles en tu Biblioteca:**\n\n${docList}\n\nPuedes consultarlos en detalle en la pestaña **Biblioteca de Activos**.`); } } } catch (e) { @@ -92,107 +288,61 @@ export async function POST(req: NextRequest) { } } - // 1. Intención: Objetivos vs Real / Estrategia - if (userMessage.includes('objetivo') || userMessage.includes('meta') || userMessage.includes('factura') || userMessage.includes('estrategia')) { - try { - const now = new Date(); - const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`; - const res = await fetch(`${supabaseUrl}/rest/v1/rpc/targets_vs_actual`, { - method: 'POST', - headers, - body: JSON.stringify({ p_month: currentMonth }) - }); - if (res.ok) { - const data = await res.json(); - const t = data[0] || {}; - const revAct = Number(t.revenue_actual || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 }); - const revTar = Number(t.revenue_target || 0).toLocaleString('es-ES'); - const text = `🎯 **Estado de Objetivos Mensuales (Mes ${currentMonth}):**\n\n` + - `- **Facturación:** ${revAct} € de ${revTar} € (${t.revenue_achievement_pct || 0}% de la meta)\n` + - `- **Leads Captados:** ${t.leads_actual || 0} de ${t.leads_target || 0} objetivo\n` + - `- **Clientes Ganados:** ${t.clients_actual || 0} de ${t.clients_target || 0} objetivo\n\n` + - `*Fuente: RPC targets_vs_actual (Supabase PostgreSQL)*`; - return Response.json([{ text }]); - } - } catch (e) { - console.error('Error in targets_vs_actual:', e); - } + // 9. Intención: Metodología y Glosario 4Ventures + if ( + query.includes('que es cac') || + query.includes('cohorte') || + query.includes('caja') || + query.includes('atribucion') || + query.includes('90 dias') || + query.includes('metodologia') + ) { + return reply( + `📖 **Metodología Oficial de Crecimiento — 4Ventures:**\n\n` + + `1. **CAC de Cohorte (Cohort CAC):**\n` + + ` Inversión en Ads del periodo dividida entre los clientes ganados que *hicieron clic por primera vez en ese mismo periodo*, sin importar si tardaron 15 o 60 días en comprar. Es la única métrica real para saber si tus campañas son rentables.\n\n` + + `2. **CAC de Caja (Cash CAC):**\n` + + ` Inversión total del mes dividida entre los clientes cerrados ese mismo mes. Sirve para controlar el flujo de tesorería mensual.\n\n` + + `3. **Atribución Universal a 90 Días (` + '`_4v_attr.js`' + `):**\n` + + ` Captura los Click IDs (` + '`fbclid`' + `, ` + '`gclid`' + `) y UTMs en el primer contacto del visitante y los mantiene 90 días en el navegador, inyectándolos en formularios y enlaces de WhatsApp.\n\n` + + `4. **Cierre de Bucle CRM (` + '`deal-won`' + `):**\n` + + ` Cuando tu equipo de ventas marca un cliente como "Ganado", n8n recupera el origen original y recalcula el ROAS al instante.` + ); } - // 2. Intención: Embudo / Funnel / Conversión - if (userMessage.includes('funnel') || userMessage.includes('embudo') || userMessage.includes('tasa') || userMessage.includes('ticket')) { - try { - const res = await fetch(`${supabaseUrl}/rest/v1/rpc/lead_funnel`, { - method: 'POST', - headers, - body: JSON.stringify({ p_tenant_id: tenantId, p_days: 30 }) - }); - if (res.ok) { - const f = await res.json(); - const text = `🔄 **Embudo de Conversión Comercial (Últimos 30 días):**\n\n` + - `- **Leads Registrados:** ${f.total_leads || 0}\n` + - `- **Leads Contactados:** ${f.contacted_leads || 0}\n` + - `- **Deals en Pipeline:** ${f.deals_in_pipeline || 0}\n` + - `- **Deals Ganados:** ${f.deals_won || 0}\n` + - `- **Facturación Cerrada:** ${Number(f.revenue_won || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €\n` + - `- **Ticket Medio:** ${Number(f.avg_ticket || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €\n` + - `- **Tasa de Cierre Lead -> Venta:** ${f.conversion_lead_to_deal_pct || 0}%\n\n` + - `*Fuente: RPC lead_funnel (Supabase PostgreSQL)*`; - return Response.json([{ text }]); - } - } catch (e) { - console.error('Error in lead_funnel:', e); - } - } - - // 3. Intención: Ranking de Campañas / Anuncios - if (userMessage.includes('campa') || userMessage.includes('anuncio') || userMessage.includes('ranking') || userMessage.includes('mejor')) { - try { - const res = await fetch(`${supabaseUrl}/rest/v1/rpc/rank_campaigns`, { - method: 'POST', - headers, - body: JSON.stringify({ p_tenant_id: tenantId, p_metric: 'spend', p_limit: 5 }) - }); - if (res.ok) { - const campaigns = await res.json(); - const list = campaigns.map((c: any) => - `- **${c.campaign_name}** (${c.platform.toUpperCase()}): Inversión ${Number(c.spend || 0).toFixed(2)} €, ${c.clicks || 0} clics, ${c.impressions || 0} imp., ${c.leads || 0} leads.` - ).join('\n'); - const text = `🏆 **Top Campañas por Inversión Publicitaria:**\n\n${list || 'No constan campañas publicitarias activas en este periodo.'}\n\n*Fuente: RPC rank_campaigns (Windsor.ai pipeline)*`; - return Response.json([{ text }]); - } - } catch (e) { - console.error('Error in rank_campaigns:', e); - } - } - - // 4. Default: Consulta de KPIs Semanales (CAC & ROAS) - let kpis: any[] = []; + // 10. Consulta de KPIs Semanales / CAC & ROAS (Default Analítico) try { - const res = await fetch(`${supabaseUrl}/rest/v1/kpi_weekly?select=*`, { - headers: { - apikey: serviceKey, - Authorization: `Bearer ${serviceKey}` - } - }); + const res = await fetch(`${supabaseUrl}/rest/v1/kpi_weekly?tenant_id=eq.${tenantId}&select=*`, { headers }); if (res.ok) { - kpis = await res.json(); + const kpis = await res.json(); + if (Array.isArray(kpis) && kpis.length > 0) { + const channelsSummary = kpis.map((k: any) => + `- Canal **${(k.channel || 'Directo').toUpperCase()}**:\n` + + ` - Inversión: **${Number(k.total_spend || 0).toFixed(2)} €**\n` + + ` - Leads Captados: **${k.total_leads || 0}**\n` + + ` - Clientes Ganados: **${k.cohort_deals_won || 0}**\n` + + ` - Facturación Cohorte: **${Number(k.cohort_revenue_won || 0).toLocaleString('es-ES', { minimumFractionDigits: 2 })} €**\n` + + ` - CAC Cohorte: **${Number(k.cac_cohort || 0).toFixed(2)} €**\n` + + ` - ROAS Cohorte: **${Number(k.roas_cohort || 0).toFixed(2)}x**` + ).join('\n\n'); + + return reply( + `📊 **Rendimiento Analítico de ${companyName} (Corte Horario):**\n\n` + + `${channelsSummary}\n\n` + + `💡 *Puedes consultarme simulaciones ("¿Qué pasa si invierto 1000€ en Meta?"), metas de facturación o el embudo de ventas.*` + ); + } } } catch (e) { console.error('Error fetching KPIs for assistant:', e); } - if (kpis.length === 0) { - return Response.json([{ - text: 'Actualmente no constan datos consolidados en la vista de KPIs para este periodo. Por favor, asegúrate de que el webhook de leads y la sincronización diaria de Windsor.ai hayan registrado datos.' - }]); - } - - const channelsSummary = kpis.map((k: any) => - `- Canal **${k.channel.toUpperCase()}**: ${k.total_leads} leads captados, inversión de ${Number(k.total_spend || 0).toFixed(2)} €, ${k.cohort_deals_won} clientes ganados, facturación de ${Number(k.cohort_revenue_won || 0).toFixed(2)} €, CAC: ${k.cac_cohort} €, ROAS: ${k.roas_cohort}x.` - ).join('\n'); - - const answer = `📊 **Resumen de Rendimiento Analítico (Fuente: kpi_weekly, corte horario):**\n\n${channelsSummary}\n\n💡 **Regla de Atribución:** Calculado según el modelo oficial de cohorte a 60 días sin alucinación. Puedes preguntarme también sobre *objetivos*, *embudo de conversión* o *mejores campañas*.`; - - return Response.json([{ text: answer }]); + return reply( + `📊 **Centro de Analítica — ${companyName}:**\n\n` + + `No he detectado una consulta numérica específica sobre tu pregunta, pero puedo proporcionarte métricas en tiempo real:\n\n` + + `- Escribe *«Objetivos»* para ver las metas de facturación vs ventas reales.\n` + + `- Escribe *«Simular 1000€ en Meta»* para proyectar nuevos clientes con intervalos p10/p50/p90.\n` + + `- Escribe *«Embudo»* para ver la tasa de conversión y ticket medio.\n` + + `- Escribe *«Campañas»* para ver los anuncios con mayor inversión.` + ); } diff --git a/src/app/dashboard/asistente/page.tsx b/src/app/dashboard/asistente/page.tsx index 4491ce3..9ffe7d1 100644 --- a/src/app/dashboard/asistente/page.tsx +++ b/src/app/dashboard/asistente/page.tsx @@ -1,7 +1,7 @@ "use client"; -import React, { useState } from 'react'; -import { Bot, Send, Sparkles } from 'lucide-react'; +import React, { useState, useRef, useEffect } from 'react'; +import { Bot, Send, Sparkles, TrendingUp, Target, Filter, DollarSign, BookOpen } from 'lucide-react'; export default function AsistentePage() { const [input, setInput] = useState(''); @@ -12,6 +12,15 @@ export default function AsistentePage() { } ]); const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages, loading]); const sendMessage = async (textToSend?: string) => { const text = textToSend || input; @@ -26,12 +35,25 @@ export default function AsistentePage() { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ messages: newMessages }) + body: JSON.stringify({ + message: text, + messages: newMessages + }) }); if (res.ok) { const data = await res.json(); - const assistantText = typeof data === 'string' ? data : (data[0]?.text || JSON.stringify(data)); + let assistantText = ''; + if (Array.isArray(data) && data[0]?.text) { + assistantText = data[0].text; + } else if (data && typeof data.text === 'string') { + assistantText = data.text; + } else if (typeof data === 'string') { + assistantText = data; + } else { + assistantText = JSON.stringify(data); + } + setMessages([...newMessages, { role: 'assistant', content: assistantText }]); } else { setMessages([...newMessages, { role: 'assistant', content: 'Lo siento, hubo un problema al procesar la consulta con las herramientas de analítica.' }]); @@ -45,6 +67,7 @@ export default function AsistentePage() { return (
+ {/* Header */}
@@ -52,11 +75,12 @@ export default function AsistentePage() {

Asistente IA Financiero & KPIs

-

Temperatura 0.0 · Verificación Numérica Estricta · Límite: 200 msg/mes

+

Temperatura 0.0 · Verificación Numérica Estricta · Supabase RPC

+ {/* Messages Feed */}
{messages.map((m, idx) => (
{m.content}
))} + {loading && (
-
+
- Consultando base de datos y calculando métricas... + Calculando proyecciones y consultando base de datos...
)} +
-
+ {/* Quick Prompts Chips */} +
+ + +
+ {/* Input Box */}
{ @@ -111,13 +162,13 @@ export default function AsistentePage() { type="text" value={input} onChange={(e) => setInput(e.target.value)} - placeholder="Pregunta sobre métricas, CAC, ROAS o cumplimiento de objetivos..." - className="flex-1 bg-[#111827] border border-gray-700 rounded-lg px-4 py-2.5 text-sm text-white focus:outline-none focus:border-brand-500" + placeholder="Escribe una pregunta sobre inversión, metas, CAC, o escribe «Hola»..." + className="flex-1 bg-[#111827] border border-gray-700 rounded-xl px-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500 transition-all" />