styling update

This commit is contained in:
2026-01-16 13:38:00 +00:00
parent d50f1f3e3e
commit 50e27434b9
3 changed files with 326 additions and 6 deletions

View File

@@ -330,8 +330,18 @@ async function handleCallback() {
function addMessage(text, type = "user") {
const msg = document.createElement("div");
msg.className = `message ${type}`;
msg.innerHTML = `<div class="message-text">${escapeHtml(text)}</div>`;
// Render markdown for bot messages, escape HTML for user messages
const content = type === "assistant" ? renderMarkdown(text) : escapeHtml(text);
msg.innerHTML = `<div class="message-text">${content}</div>`;
chatMessages.appendChild(msg);
// Add copy buttons to code blocks for assistant messages
if (type === "assistant") {
addCopyButtonsToCodeBlocks(msg);
}
scrollToBottom();
}
@@ -367,6 +377,87 @@ function escapeHtml(text) {
return div.innerHTML;
}
function renderMarkdown(text) {
// Check if libraries are loaded
if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
logger.error('Markdown libraries not loaded', {
markedAvailable: typeof marked !== 'undefined',
dompurifyAvailable: typeof DOMPurify !== 'undefined'
});
return escapeHtml(text);
}
try {
// Configure marked.js for optimal chat rendering
marked.setOptions({
breaks: true, // Convert line breaks to <br>
gfm: true, // GitHub-flavored markdown
headerIds: false, // No IDs for headers
mangle: false // Don't obfuscate emails
});
// Parse markdown to HTML
const rawHtml = marked.parse(text);
// Sanitize HTML to prevent XSS attacks (include button element for copy functionality)
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'button'],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class']
});
return cleanHtml;
} catch (error) {
logger.error('Markdown rendering failed', error);
return escapeHtml(text);
}
}
function addCopyButtonsToCodeBlocks(element) {
// Find all <pre> elements in the given element
const preElements = element.querySelectorAll('pre');
preElements.forEach((pre) => {
// Skip if button already exists
if (pre.querySelector('.copy-button')) {
return;
}
// Create copy button
const button = document.createElement('button');
button.className = 'copy-button';
button.textContent = 'Copy';
// Add click handler
button.addEventListener('click', async () => {
const code = pre.querySelector('code');
const text = code ? code.textContent : pre.textContent;
try {
await navigator.clipboard.writeText(text);
button.textContent = 'Copied!';
button.classList.add('copied');
// Reset after 2 seconds
setTimeout(() => {
button.textContent = 'Copy';
button.classList.remove('copied');
}, 2000);
} catch (error) {
logger.error('Failed to copy code to clipboard', error);
button.textContent = 'Failed';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
}
});
// Add button to pre element
pre.appendChild(button);
});
}
async function sendMessage(text) {
if (!text.trim()) return;
@@ -438,11 +529,13 @@ async function sendMessage(text) {
if (data.chunk) {
assistantMessage += data.chunk;
textDiv.textContent = assistantMessage;
textDiv.innerHTML = renderMarkdown(assistantMessage);
scrollToBottom();
}
if (data.done) {
// Add copy buttons to code blocks after streaming is complete
addCopyButtonsToCodeBlocks(msgDiv);
break;
}
}