styling update
This commit is contained in:
@@ -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>
|
||||
|
||||
97
script.js
97
script.js
@@ -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
230
style.css
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user