feat(mejoras): 22 mejoras UX en todos los módulos
Mi día: cancelación con motivo, conteo escrituración real, cita sin retroceder, calendario completo Cobranza: MoraDashboard respeta alcance, hoy dinámico, cancelar con motivo Landing: contactos paginados, error vs vacío diferenciado Dashboards: KPI renombrado, drill-down planes activos Chat: historial recarga, preguntas rotativas, error solo toast Transversal: shadow-sm corrupto corregido en 9 archivos 553 API tests + 7 apps construidas
This commit is contained in:
24
apps/agente-ia/src/lib/vocabulario.test.ts
Normal file
24
apps/agente-ia/src/lib/vocabulario.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { DESARROLLOS, preguntasSugeridas } from './vocabulario'
|
||||
|
||||
describe('preguntasSugeridas', () => {
|
||||
it('devuelve 4 preguntas, cada una de un desarrollo distinto', () => {
|
||||
const preguntas = preguntasSugeridas(new Date('2026-09-11T12:00:00'))
|
||||
expect(preguntas).toHaveLength(4)
|
||||
const usados = DESARROLLOS.filter((d) => preguntas.some((p) => p.includes(d)))
|
||||
expect(usados).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('rota día con día: dos fechas consecutivas no comparten desarrollo en la primera pregunta', () => {
|
||||
const lunes = preguntasSugeridas(new Date('2026-09-07T00:00:00'))
|
||||
const martes = preguntasSugeridas(new Date('2026-09-08T00:00:00'))
|
||||
expect(lunes[0]).not.toBe(martes[0])
|
||||
})
|
||||
|
||||
it('tras DESARROLLOS.length días vuelve a la combinación inicial', () => {
|
||||
const base = new Date('2026-01-01T00:00:00')
|
||||
const inicial = preguntasSugeridas(base)
|
||||
const vuelta = preguntasSugeridas(new Date(base.getTime() + DESARROLLOS.length * 86_400_000))
|
||||
expect(vuelta).toEqual(inicial)
|
||||
})
|
||||
})
|
||||
33
apps/agente-ia/src/lib/vocabulario.ts
Normal file
33
apps/agente-ia/src/lib/vocabulario.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Espejo del vocabulario del API (api-gateway/src/config/vocabulario.ts):
|
||||
* DESARROLLOS es la lista canónica de desarrollos FRASA. Mantener sincronizado.
|
||||
*/
|
||||
export const DESARROLLOS = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const
|
||||
|
||||
/** Plantillas de preguntas sugeridas, parametrizadas por desarrollo */
|
||||
const PLANTILLAS: ((desarrollo: string) => string)[] = [
|
||||
(d) => `¿Qué modelos hay en el catálogo ${d}?`,
|
||||
(d) => `¿Qué incluye la propuesta del cotizador ${d}?`,
|
||||
(d) => `¿Cuál es la disponibilidad de unidades en ${d}?`,
|
||||
(d) => `¿Qué amenidades y ubicación tiene ${d}?`,
|
||||
]
|
||||
|
||||
/** Día del año (1..366) para la rotación diaria */
|
||||
function diaDelAnio(fecha: Date): number {
|
||||
const inicio = Date.UTC(fecha.getFullYear(), 0, 1)
|
||||
const actual = Date.UTC(fecha.getFullYear(), fecha.getMonth(), fecha.getDate())
|
||||
return Math.round((actual - inicio) / 86_400_000) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Preguntas sugeridas rotativas: cada una toma un desarrollo distinto
|
||||
* (VOCABULARIO.DESARROLLOS) y la combinación avanza día con día, de modo que
|
||||
* las sugerencias del chat rotan por desarrollo sin repetirse el mismo día.
|
||||
*/
|
||||
export function preguntasSugeridas(fecha: Date = new Date()): string[] {
|
||||
const dia = diaDelAnio(fecha)
|
||||
return PLANTILLAS.map((plantilla, i) => {
|
||||
const desarrollo = DESARROLLOS[(dia + i) % DESARROLLOS.length]!
|
||||
return plantilla(desarrollo)
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Burbuja from '../components/Burbuja'
|
||||
import DarkModeToggle from '../components/DarkModeToggle'
|
||||
import toast from "react-hot-toast"
|
||||
@@ -10,25 +10,20 @@ import {
|
||||
type ConversacionResumen,
|
||||
} from '../services/api'
|
||||
import { cargarSesion, guardarSesion, limpiarSesion } from '../lib/sesion'
|
||||
import { preguntasSugeridas } from '../lib/vocabulario'
|
||||
import type { Mensaje } from '../types'
|
||||
|
||||
let contadorLocal = 0
|
||||
const idLocal = () => `local-${++contadorLocal}`
|
||||
|
||||
const PREGUNTAS_SUGERIDAS = [
|
||||
'¿Qué incluye la propuesta del cotizador DIAGONAL?',
|
||||
'¿Cuáles son los pasos del proceso de venta?',
|
||||
'¿Qué políticas de Frasa debo conocer?',
|
||||
'¿Qué modelos hay en el catálogo DIAGONAL?',
|
||||
]
|
||||
|
||||
export default function ChatPage() {
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
const [mensajes, setMensajes] = useState<Mensaje[]>([])
|
||||
const [entrada, setEntrada] = useState('')
|
||||
const [escribiendo, setEscribiendo] = useState(false)
|
||||
const [sinConexion, setSinConexion] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Preguntas sugeridas del día: rotan por desarrollo (VOCABULARIO.DESARROLLOS)
|
||||
const preguntasSugeridasHoy = useMemo(() => preguntasSugeridas(), [])
|
||||
// Texto del último envío fallido (habilita "Reintentar" junto al input)
|
||||
const [pendiente, setPendiente] = useState<string | null>(null)
|
||||
// Panel de conversaciones previas
|
||||
@@ -70,7 +65,7 @@ export default function ChatPage() {
|
||||
if (esErrorDeRed(err)) {
|
||||
setSinConexion(true)
|
||||
} else {
|
||||
toast.error('Error al enviar el mensaje'); setError(err instanceof Error ? err.message : 'Error al enviar el mensaje')
|
||||
toast.error('Error al enviar el mensaje')
|
||||
}
|
||||
setPendiente(texto) // el mensaje del usuario se queda visible; puede reintentar
|
||||
} finally {
|
||||
@@ -82,7 +77,6 @@ export default function ChatPage() {
|
||||
const texto = entrada.trim()
|
||||
if (!texto || escribiendo) return
|
||||
setEntrada('')
|
||||
setError(null)
|
||||
setMensajes((prev) => [...prev, { id: idLocal(), rol: 'user', contenido: texto, fuentes: [] }])
|
||||
await transmitir(texto)
|
||||
}
|
||||
@@ -90,7 +84,6 @@ export default function ChatPage() {
|
||||
/** Reenvía el último mensaje que falló (sin duplicar la burbuja del usuario) */
|
||||
async function reintentar() {
|
||||
if (!pendiente || escribiendo) return
|
||||
setError(null)
|
||||
setSinConexion(false)
|
||||
await transmitir(pendiente)
|
||||
}
|
||||
@@ -99,15 +92,14 @@ export default function ChatPage() {
|
||||
limpiarSesion()
|
||||
setSessionId(null)
|
||||
setMensajes([])
|
||||
setError(null)
|
||||
setSinConexion(false)
|
||||
setPendiente(null)
|
||||
}
|
||||
|
||||
/** Abre el panel y carga (una vez) el listado de conversaciones del usuario */
|
||||
/** Abre el panel y recarga SIEMPRE el listado de conversaciones del usuario */
|
||||
function abrirPanel() {
|
||||
setPanelAbierto(true)
|
||||
if (conversaciones !== null || cargandoConversaciones) return
|
||||
if (cargandoConversaciones) return
|
||||
setCargandoConversaciones(true)
|
||||
listarConversaciones()
|
||||
.then(setConversaciones)
|
||||
@@ -123,7 +115,6 @@ export default function ChatPage() {
|
||||
guardarSesion(id)
|
||||
setMensajes(historial)
|
||||
setPendiente(null)
|
||||
setError(null)
|
||||
setSinConexion(false)
|
||||
setPanelAbierto(false)
|
||||
} catch {
|
||||
@@ -132,7 +123,7 @@ export default function ChatPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-[#F8FAFC] text-[#111827] dark:text-[#E2E8F0] dark:bg-[#0F172A] dark:text-[#E2E8F0]">
|
||||
<div className="flex min-h-screen flex-col bg-[#F8FAFC] text-[#111827] dark:text-[#E2E8F0] dark:bg-[#0F172A]">
|
||||
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur dark:border-[#1E293B] dark:bg-[#1E293B]/95">
|
||||
<div className="mx-auto flex h-14 w-full max-w-3xl items-center justify-between px-4 sm:px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -176,11 +167,6 @@ export default function ChatPage() {
|
||||
nuevo.
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p className="mb-6 rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{mensajes.length === 0 && !escribiendo ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
@@ -193,7 +179,7 @@ export default function ChatPage() {
|
||||
venta o las políticas de Frasa.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-2 sm:grid-cols-2">
|
||||
{PREGUNTAS_SUGERIDAS.map((pregunta) => (
|
||||
{preguntasSugeridasHoy.map((pregunta) => (
|
||||
<button
|
||||
key={pregunta}
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user