feat(ai): agente IA interno con RAG sobre NAN + app web en /ai

- API: /api/ai/chat, /conversations, /documents con embeddings qwen3-embedding,
  coseno en SQLite, dedup por hash, historial de conversaciones
- Base de conocimiento semilla (6 docs, 36 chunks): catálogo DIAGONAL,
  propuestas del cotizador, créditos, proceso de venta, FAQ, privacidad
- App agente-ia (Vite/React/Tailwind, StudioBlank) servida en /ai/ vía nginx
- 21 tests nuevos (91/91 verdes)
This commit is contained in:
2026-08-22 03:04:47 +00:00
parent 8407fc24c3
commit 05c44478ec
32 changed files with 5448 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { cargarSesion, guardarSesion, limpiarSesion, type StorageLike } from './sesion'
function storageFalso(): StorageLike & { datos: Map<string, string> } {
const datos = new Map<string, string>()
return {
datos,
getItem: (k) => datos.get(k) ?? null,
setItem: (k, v) => void datos.set(k, v),
removeItem: (k) => void datos.delete(k),
}
}
describe('sesion del chat (localStorage)', () => {
it('devuelve null cuando no hay sesión guardada o está vacía', () => {
const s = storageFalso()
expect(cargarSesion(s)).toBeNull()
s.setItem('frasa-ai:sessionId', '')
expect(cargarSesion(s)).toBeNull()
})
it('guarda y recupera el sessionId', () => {
const s = storageFalso()
guardarSesion('conv-123', s)
expect(cargarSesion(s)).toBe('conv-123')
})
it('limpiarSesion elimina el sessionId (nueva conversación)', () => {
const s = storageFalso()
guardarSesion('conv-123', s)
limpiarSesion(s)
expect(cargarSesion(s)).toBeNull()
})
})

View File

@@ -0,0 +1,22 @@
// Sesión del chat persistida en localStorage: al recargar, la conversación sigue viva.
const CLAVE_SESION = 'frasa-ai:sessionId'
/** Subconjunto de Storage para poder probar sin DOM */
export interface StorageLike {
getItem(clave: string): string | null
setItem(clave: string, valor: string): void
removeItem(clave: string): void
}
export function cargarSesion(storage: StorageLike = localStorage): string | null {
const id = storage.getItem(CLAVE_SESION)
return id && id.length > 0 ? id : null
}
export function guardarSesion(sessionId: string, storage: StorageLike = localStorage): void {
storage.setItem(CLAVE_SESION, sessionId)
}
export function limpiarSesion(storage: StorageLike = localStorage): void {
storage.removeItem(CLAVE_SESION)
}