From b94bbe2f3e6aea3dddec964769946345ceb191f4 Mon Sep 17 00:00:00 2001 From: jae Date: Mon, 20 Apr 2026 10:57:19 +0000 Subject: [PATCH] fix(chat): render markdown in JAE-AI responses (bold/italic/code/links/headings) --- css/agent-chat.css | 55 ++++++++++++++++++++++++++++++++++++++++++++++ js/chat.js | 46 ++++++++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/css/agent-chat.css b/css/agent-chat.css index 17ca372..97a10f0 100644 --- a/css/agent-chat.css +++ b/css/agent-chat.css @@ -143,3 +143,58 @@ font-size: 0.72rem; } } + +/* ========================================= + PHASE 2 — Markdown rendering in chat + ========================================= */ +.chat-msg-body strong { color: var(--accent); font-weight: 700; } +.chat-msg-body em { color: var(--text-dim); font-style: italic; } +.chat-msg-body code { + background: rgba(20, 241, 149, 0.1); + padding: 0 0.3rem; + border-radius: 2px; + color: var(--status-green, #14f195); + font-family: 'JetBrains Mono', 'Courier New', monospace; + font-size: 0.85em; + border: 1px solid rgba(20, 241, 149, 0.2); +} +.chat-msg-body pre { + background: rgba(0, 0, 0, 0.45); + padding: 0.6rem 0.75rem; + border-radius: 4px; + overflow-x: auto; + border-left: 2px solid var(--accent); + margin: 0.5rem 0; + max-width: 100%; +} +.chat-msg-body pre code { + background: transparent; + border: none; + padding: 0; + color: var(--text, #e8e8e8); + font-size: 0.8em; + white-space: pre; +} +.chat-msg-body a { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + transition: color 0.15s ease; +} +.chat-msg-body a:hover { color: var(--status-green, #14f195); } +.chat-msg-body h2, +.chat-msg-body h3, +.chat-msg-body h4 { + color: var(--accent); + margin: 0.5rem 0 0.25rem; + font-weight: 600; + letter-spacing: 0.5px; +} +.chat-msg-body h2 { font-size: 1.1em; } +.chat-msg-body h3 { font-size: 1.0em; } +.chat-msg-body h4 { font-size: 0.95em; } +.chat-msg-body ul, .chat-msg-body ol { + margin: 0.3rem 0 0.3rem 1.25rem; + padding: 0; +} +.chat-msg-body li { margin: 0.15rem 0; list-style: disc; } diff --git a/js/chat.js b/js/chat.js index a7b0a09..5924ba8 100644 --- a/js/chat.js +++ b/js/chat.js @@ -28,6 +28,36 @@ }); } + // ─── Minimal safe Markdown → HTML (assistant messages only) ── + function md(text) { + if (!text) return ''; + let html = String(text).replace(/&/g, '&').replace(//g, '>'); + // Fenced code blocks ```...``` + html = html.replace(/```([\s\S]*?)```/g, function (_m, code) { + return '
' + code + '
'; + }); + // Inline code + html = html.replace(/`([^`\n]+)`/g, '$1'); + // Bold **...** + html = html.replace(/\*\*([^*\n]+)\*\*/g, '$1'); + // Italic *...* (avoid double-asterisk leftovers) + html = html.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1$2'); + // Links [text](url) + html = html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '$1'); + // Headings + html = html.replace(/^### (.+)$/gm, '

$1

'); + html = html.replace(/^## (.+)$/gm, '

$1

'); + html = html.replace(/^# (.+)$/gm, '

$1

'); + // Bullet list lines: "- item" / "* item" →
  • + html = html.replace(/^(?:-|\*) (.+)$/gm, '
  • $1
  • '); + html = html.replace(/(
  • [\s\S]*?<\/li>)(?!\s*
  • )/g, ''); + // Line breaks (avoid inside
    )
    +        html = html.replace(/(
    [\s\S]*?<\/pre>)|\n/g, function (m, pre) {
    +            return pre ? pre : '
    '; + }); + return html; + } + // ─── Render a message bubble ────────────────────── function addMessage(role, text) { const welcome = chatMessages.querySelector('.chat-welcome'); @@ -42,7 +72,7 @@ const body = document.createElement('div'); body.className = 'chat-msg-body'; - body.textContent = text; + if (role === 'assistant') { body.innerHTML = md(text); } else { body.textContent = text; } msg.appendChild(label); msg.appendChild(body); @@ -101,22 +131,24 @@ // ─── Typewriter effect ──────────────────────────── function typewriterEffect(element, text, speed) { speed = speed || 10; - // If reply is too long, bypass animation + element.innerHTML = ''; + // Bypass animation for long replies — render full markdown immediately if (!text || text.length > 600) { - element.textContent = text || ''; + element.innerHTML = md(text || ''); chatMessages.scrollTop = chatMessages.scrollHeight; return Promise.resolve(); } let i = 0; - element.textContent = ''; return new Promise(function (resolve) { function tick() { if (i < text.length) { - element.textContent += text.charAt(i); i++; + element.innerHTML = md(text.slice(0, i)); chatMessages.scrollTop = chatMessages.scrollHeight; setTimeout(tick, speed); } else { + // Dispatch event so voice-mode / others can hook + try { document.dispatchEvent(new CustomEvent('jae-agent-reply', { detail: { text: text } })); } catch (e) {} resolve(); } } @@ -138,7 +170,7 @@ label.textContent = role === 'user' ? 'YOU' : 'JAE-AI'; const body = document.createElement('div'); body.className = 'chat-msg-body'; - body.textContent = m.content || ''; + if (role === 'assistant') { body.innerHTML = md(m.content || ''); } else { body.textContent = m.content || ''; } const el = document.createElement('div'); el.className = `chat-msg chat-msg-${role}`; el.appendChild(label); @@ -296,7 +328,7 @@ const body = addMessage('assistant', ''); const msgEl = body && body.parentElement; if (msgEl) msgEl.classList.add('chat-msg-cli'); - body.textContent = res.output; + body.innerHTML = md(res.output); chatMessages.scrollTop = chatMessages.scrollHeight; } }