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:
- 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. - 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ódulo | Función/endpoint | Cuándo aplica |
|---|---|---|
core/api_help.py (Help Widget) | help_ask / help_ask_v2 | Al devolver respuesta al usuario (commit 7570937a) |
monitoring/api/insights.py | explain_insight | Al devolver respuesta del CNS Explain (commit d26e07e) |
monitoring/api/insight_conversations.py | list_conversations | Al 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 <strong> en pantalla
answerDiv.innerHTML = escHtml(resp.answer);
Ficheros JS afectados
static/js/utils/InsightActions.js—handleExplain: cambiado detextContentainnerHTML(commitd26e07e).static/js/utils/InsightDetailModal.js—_renderConvEntry: eliminadoescHtml(conv.answer)ywhite-space:pre-wrap(commitd26e07e).- 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:
- Importar
_md_to_htmldesdecore.api_help. - Aplicarla sobre el campo
answer/contentantes de incluirlo en el JSON de respuesta. - En el template/JS consumidor, usar
innerHTML(odangerouslySetInnerHTMLen React) sin escape adicional. - 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]]