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

@@ -63,6 +63,9 @@
<div id="debugOutput" style="margin-top: 10px; max-height: 200px; overflow-y: auto;"></div>
</div>
<script src="script.js?v=2"></script>
<!-- Markdown rendering libraries (load before main script) -->
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<script src="script.js?v=4"></script>
</body>
</html>

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

230
style.css
View File

@@ -177,7 +177,7 @@ kbd {
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px 0;
padding: 16px;
}
.chat-messages::-webkit-scrollbar {
@@ -199,8 +199,9 @@ kbd {
/* ==================== MESSAGES ==================== */
.message {
font-size: 13px;
line-height: 1.6;
padding: 8px 16px;
line-height: 1.9;
padding: 12px 24px;
margin-bottom: 16px;
border-left: 3px solid transparent;
opacity: 0;
animation: fadeIn 0.2s ease forwards;
@@ -229,6 +230,229 @@ kbd {
color: var(--ctp-subtext1);
}
/* ==================== MARKDOWN ELEMENTS ==================== */
/* Styles for markdown-rendered content in assistant messages */
.message.assistant .message-text p {
margin: 0.8em 0;
}
.message.assistant .message-text p:first-child {
margin-top: 0;
}
.message.assistant .message-text p:last-child {
margin-bottom: 0;
}
/* Headers */
.message.assistant .message-text h1,
.message.assistant .message-text h2,
.message.assistant .message-text h3,
.message.assistant .message-text h4,
.message.assistant .message-text h5,
.message.assistant .message-text h6 {
color: var(--ctp-text);
}
.message.assistant .message-text h1 {
font-size: 2em;
font-weight: 700;
margin: 1.5em 0 0.6em 0;
}
.message.assistant .message-text h2 {
font-size: 1.6em;
font-weight: 700;
margin: 1.2em 0 0.5em 0;
}
.message.assistant .message-text h3 {
font-size: 1.3em;
font-weight: 600;
margin: 1em 0 0.4em 0;
}
.message.assistant .message-text h4 {
font-size: 1.15em;
font-weight: 600;
margin: 1em 0 0.4em 0;
}
.message.assistant .message-text h5 {
font-size: 1em;
font-weight: 500;
margin: 0.8em 0 0.3em 0;
}
.message.assistant .message-text h6 {
font-size: 0.9em;
font-weight: 500;
margin: 0.8em 0 0.3em 0;
}
/* First header in message should have no top margin */
.message.assistant .message-text h1:first-child,
.message.assistant .message-text h2:first-child,
.message.assistant .message-text h3:first-child,
.message.assistant .message-text h4:first-child,
.message.assistant .message-text h5:first-child,
.message.assistant .message-text h6:first-child {
margin-top: 0;
}
/* Inline code */
.message.assistant .message-text code {
background: var(--ctp-surface0);
color: var(--ctp-pink);
padding: 2px 6px;
border-radius: 3px;
font-family: inherit;
font-size: 0.95em;
}
/* Code blocks */
.message.assistant .message-text pre {
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-left: 3px solid var(--ctp-mauve);
padding: 16px;
margin: 1.2em 0;
overflow-x: auto;
border-radius: 6px;
position: relative;
}
.message.assistant .message-text pre code {
background: transparent;
color: var(--ctp-text);
padding: 0;
border-radius: 0;
font-size: 0.9em;
}
/* Lists */
.message.assistant .message-text ul,
.message.assistant .message-text ol {
margin: 1em 0;
padding-left: 2.5em;
}
.message.assistant .message-text li {
margin: 0.5em 0;
}
.message.assistant .message-text ul {
list-style-type: disc;
}
.message.assistant .message-text ol {
list-style-type: decimal;
}
/* Custom bullet colors */
.message.assistant .message-text ul li::marker {
color: var(--ctp-mauve);
}
.message.assistant .message-text ol li::marker {
color: var(--ctp-mauve);
font-weight: 600;
}
/* Links */
.message.assistant .message-text a {
color: var(--ctp-sapphire);
text-decoration: underline;
transition: color 0.15s ease;
}
.message.assistant .message-text a:hover {
color: var(--ctp-sky);
}
/* Blockquotes */
.message.assistant .message-text blockquote {
border-left: 3px solid var(--ctp-overlay0);
padding-left: 12px;
margin: 0.8em 0;
color: var(--ctp-subtext0);
font-style: italic;
}
/* Horizontal rule */
.message.assistant .message-text hr {
border: none;
border-top: 1px solid var(--ctp-surface1);
margin: 1em 0;
}
/* Tables */
.message.assistant .message-text table {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
}
.message.assistant .message-text th,
.message.assistant .message-text td {
border: 1px solid var(--ctp-surface1);
padding: 6px 10px;
text-align: left;
}
.message.assistant .message-text th {
background: var(--ctp-surface0);
color: var(--ctp-text);
font-weight: 600;
}
/* Strong and emphasis */
.message.assistant .message-text strong {
font-weight: 700;
color: var(--ctp-text);
}
.message.assistant .message-text em {
font-style: italic;
}
/* Copy button for code blocks */
.copy-button {
position: absolute;
top: 8px;
right: 8px;
background: var(--ctp-surface1);
color: var(--ctp-text);
border: 1px solid var(--ctp-surface2);
padding: 4px 12px;
font-size: 11px;
font-family: inherit;
border-radius: 4px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease, background 0.15s ease;
}
.message.assistant .message-text pre:hover .copy-button {
opacity: 1;
}
.copy-button:hover {
background: var(--ctp-surface2);
border-color: var(--ctp-mauve);
}
.copy-button:active {
background: var(--ctp-surface0);
}
.copy-button.copied {
background: var(--ctp-green);
color: var(--ctp-base);
border-color: var(--ctp-green);
}
/* ==================== CHAT INPUT ==================== */
.chat-input {
padding: 16px;