fix(chat): render markdown in JAE-AI responses (bold/italic/code/links/headings)

This commit is contained in:
jae 2026-04-20 10:57:19 +00:00
parent bc3fdf28b8
commit b94bbe2f3e
2 changed files with 94 additions and 7 deletions

View file

@ -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; }

View file

@ -28,6 +28,36 @@
});
}
// ─── Minimal safe Markdown → HTML (assistant messages only) ──
function md(text) {
if (!text) return '';
let html = String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Fenced code blocks ```...```
html = html.replace(/```([\s\S]*?)```/g, function (_m, code) {
return '<pre><code>' + code + '</code></pre>';
});
// Inline code
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>');
// Bold **...**
html = html.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
// Italic *...* (avoid double-asterisk leftovers)
html = html.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
// Links [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
// Headings
html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>');
// Bullet list lines: "- item" / "* item" → <li>
html = html.replace(/^(?:-|\*) (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>[\s\S]*?<\/li>)(?!\s*<li>)/g, '<ul>$1</ul>');
// Line breaks (avoid inside <pre>)
html = html.replace(/(<pre>[\s\S]*?<\/pre>)|\n/g, function (m, pre) {
return pre ? pre : '<br>';
});
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;
}
}