CreaRack-SL

Patrón de renderizado Markdown → HTML en CNS y Help Widget

Descripción

CreaRack Pro utiliza un patrón centralizado para convertir las respuestas Markdown de los LLMs (Gemma, Gemini, Claude…) en HTML seguro antes de enviarlas al frontend. Este patrón garantiza que elementos como **negrita**, * listas o > citas se rendericen correctamente en el navegador sin mostrar asteriscos literales.

El patrón se aplica únicamente en capa de presentación: la base de datos siempre almacena el Markdown crudo tal como lo devuelve el LLM. La transformación ocurre en el momento de servir la respuesta al cliente.


Función central: _md_to_html

Ubicación: core/api_help.py

La función _md_to_html realiza la conversión en dos pasos:

  1. Escape de HTML: aplica django.utils.html.escape() sobre el texto crudo del LLM, eliminando cualquier tag HTML malicioso que pudiera venir en la respuesta. Esto previene XSS.
  2. Sustitución por regex: convierte las marcas Markdown escapadas en etiquetas HTML seguras (<strong>, <em>, <ul>, <li>, <blockquote>, etc.).
# Importación en los módulos consumidores
from core.api_help import _md_to_html

# Uso
html_answer = _md_to_html(raw_markdown_from_llm)

Sin riesgo XSS: el escape previo garantiza que aunque el LLM inyecte <script>, <img onerror=...> u otros vectores, quedan neutralizados antes de que las regex de Markdown puedan introducirlos en el HTML de salida. Es el mismo mecanismo empleado en el Help Widget desde el commit 7570937a.


Puntos de aplicación actuales

MóduloFunción/endpointCuándo aplica
core/api_help.py (Help Widget)help_ask / help_ask_v2Al devolver respuesta al usuario (commit 7570937a)
monitoring/api/insights.pyexplain_insightAl devolver respuesta del CNS Explain (commit d26e07e)
monitoring/api/insight_conversations.pylist_conversationsAl serializar el historial de conversaciones del insight (commit d26e07e)

Contrato frontend

El frontend que consuma estos endpoints debe usar innerHTML (no textContent ni escHtml) para insertar la respuesta:

// ✅ Correcto — el backend ya escapó; el HTML se renderiza
answerDiv.innerHTML = resp.answer;

// ❌ Incorrecto — mostraría las etiquetas como texto literal
answerDiv.textContent = resp.answer;

// ❌ Incorrecto — doble escape; mostraría &lt;strong&gt; en pantalla
answerDiv.innerHTML = escHtml(resp.answer);

Ficheros JS afectados

  • static/js/utils/InsightActions.js — handleExplain: cambiado de textContent a innerHTML (commit d26e07e).
  • static/js/utils/InsightDetailModal.js — _renderConvEntry: eliminado escHtml(conv.answer) y white-space:pre-wrap (commit d26e07e).
  • Help Widget frontend — cambio análogo introducido en commit 7570937a.

Motivación y contexto histórico

El bug original fue introducido por la activación de Gemma 4 paid tier en CNS Explain (commit f47d1bd4). Los modelos de pago devuelven respuestas más ricas en formato Markdown (negritas, listas, citas), lo que expuso que el pipeline de presentación seguía tratando la respuesta como texto plano.

El patrón ya existía en Help Widget; este commit lo extiende simétricamente a CNS Explain e historial de conversaciones.


Guía de extensión

Cuando se añada un nuevo endpoint que devuelva texto de un LLM al frontend:

  1. Importar _md_to_html desde core.api_help.
  2. Aplicarla sobre el campo answer / content antes de incluirlo en el JSON de respuesta.
  3. En el template/JS consumidor, usar innerHTML (o dangerouslySetInnerHTML en React) sin escape adicional.
  4. La BD sigue recibiendo el Markdown crudo — no modificar el modelo ni las migraciones.

Véase también

  • [[feature—monitoring—cns-v1]]
  • [[concept—saas—multi-tenancy]]
  • [[entity—monitoring—model—ai-insight]]
  • [[entity—monitoring—model—insight-conversation]]
  • [[decision—20260507—selfhost-llm-server]]