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 id="debugOutput" style="margin-top: 10px; max-height: 200px; overflow-y: auto;"></div>
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
97
script.js
97
script.js
@@ -330,8 +330,18 @@ async function handleCallback() {
|
|||||||
function addMessage(text, type = "user") {
|
function addMessage(text, type = "user") {
|
||||||
const msg = document.createElement("div");
|
const msg = document.createElement("div");
|
||||||
msg.className = `message ${type}`;
|
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);
|
chatMessages.appendChild(msg);
|
||||||
|
|
||||||
|
// Add copy buttons to code blocks for assistant messages
|
||||||
|
if (type === "assistant") {
|
||||||
|
addCopyButtonsToCodeBlocks(msg);
|
||||||
|
}
|
||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +377,87 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
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) {
|
async function sendMessage(text) {
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
|
|
||||||
@@ -438,11 +529,13 @@ async function sendMessage(text) {
|
|||||||
|
|
||||||
if (data.chunk) {
|
if (data.chunk) {
|
||||||
assistantMessage += data.chunk;
|
assistantMessage += data.chunk;
|
||||||
textDiv.textContent = assistantMessage;
|
textDiv.innerHTML = renderMarkdown(assistantMessage);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.done) {
|
if (data.done) {
|
||||||
|
// Add copy buttons to code blocks after streaming is complete
|
||||||
|
addCopyButtonsToCodeBlocks(msgDiv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
230
style.css
230
style.css
@@ -177,7 +177,7 @@ kbd {
|
|||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 16px 0;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages::-webkit-scrollbar {
|
.chat-messages::-webkit-scrollbar {
|
||||||
@@ -199,8 +199,9 @@ kbd {
|
|||||||
/* ==================== MESSAGES ==================== */
|
/* ==================== MESSAGES ==================== */
|
||||||
.message {
|
.message {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.9;
|
||||||
padding: 8px 16px;
|
padding: 12px 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: fadeIn 0.2s ease forwards;
|
animation: fadeIn 0.2s ease forwards;
|
||||||
@@ -229,6 +230,229 @@ kbd {
|
|||||||
color: var(--ctp-subtext1);
|
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 ==================== */
|
||||||
.chat-input {
|
.chat-input {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user