Upload 4 new, 6 modified files from chat-switchboard-v0.5.1.zip
This commit is contained in:
106
src/js/ui.js
106
src/js/ui.js
@@ -425,46 +425,88 @@ const UI = {
|
||||
|
||||
// ── Formatting ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Format message content with markdown rendering.
|
||||
* Uses marked.js + DOMPurify when available, regex fallback otherwise.
|
||||
*/
|
||||
function formatMessage(content) {
|
||||
if (!content) return '';
|
||||
const thinkingBlocks = [];
|
||||
|
||||
// ── Extract thinking blocks (both <thinking> and <think> tags) ──
|
||||
const thinkingBlocks = [];
|
||||
let text = content;
|
||||
|
||||
// Extract thinking blocks
|
||||
if (App.settings.showThinking) {
|
||||
text = text.replace(/<thinking>([\s\S]*?)<\/thinking>/gi, (_, inner) => {
|
||||
const id = 'think-' + Math.random().toString(36).slice(2, 9);
|
||||
thinkingBlocks.push({ id, content: inner.trim() });
|
||||
return `__THINK_${id}__`;
|
||||
});
|
||||
} else {
|
||||
text = text.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
text = esc(text);
|
||||
|
||||
// Restore thinking blocks (single-escaped)
|
||||
for (const b of thinkingBlocks) {
|
||||
const inner = esc(b.content).replace(/\n/g, '<br>');
|
||||
text = text.replace(`__THINK_${b.id}__`,
|
||||
`<details class="thinking-block"><summary>💭 Thinking</summary><div class="thinking-content">${inner}</div></details>`);
|
||||
}
|
||||
|
||||
// Code blocks
|
||||
text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
const id = 'code-' + Math.random().toString(36).slice(2, 9);
|
||||
return `<pre><code id="${id}" class="lang-${lang}">${code.trim()}</code><button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${id}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button></pre>`;
|
||||
// Pull out thinking blocks before any processing
|
||||
text = text.replace(/<(?:thinking|think)>([\s\S]*?)<\/(?:thinking|think)>/gi, (_, inner) => {
|
||||
const id = 'think-' + Math.random().toString(36).slice(2, 9);
|
||||
thinkingBlocks.push({ id, content: inner.trim() });
|
||||
return `THINK_PLACEHOLDER_${id}`;
|
||||
});
|
||||
|
||||
// Inline formatting
|
||||
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
// ── Render markdown ──
|
||||
let html;
|
||||
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
||||
html = _formatMarked(text);
|
||||
} else {
|
||||
html = _formatBasic(text);
|
||||
}
|
||||
|
||||
return text;
|
||||
// ── Restore thinking blocks ──
|
||||
for (const b of thinkingBlocks) {
|
||||
const inner = esc(b.content).replace(/\n/g, '<br>');
|
||||
const thinkHTML = App.settings.showThinking
|
||||
? `<details class="thinking-block"><summary>💭 Thinking</summary><div class="thinking-content">${inner}</div></details>`
|
||||
: '';
|
||||
|
||||
// marked may wrap placeholder in <p> tags
|
||||
html = html.replace(new RegExp(`<p>\\s*THINK_PLACEHOLDER_${b.id}\\s*</p>`, 'g'), thinkHTML);
|
||||
html = html.replace(`THINK_PLACEHOLDER_${b.id}`, thinkHTML);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full markdown rendering via marked.js + DOMPurify
|
||||
*/
|
||||
function _formatMarked(text) {
|
||||
const rendered = marked.parse(text, {
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
// Sanitize but allow code structure
|
||||
let html = DOMPurify.sanitize(rendered, {
|
||||
ADD_TAGS: ['details', 'summary'],
|
||||
ADD_ATTR: ['id', 'class', 'onclick']
|
||||
});
|
||||
|
||||
// Inject copy buttons into <pre><code> blocks
|
||||
html = html.replace(/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/g, (_, attrs, code) => {
|
||||
const codeId = 'code-' + Math.random().toString(36).slice(2, 9);
|
||||
return `<pre><code id="${codeId}"${attrs}>${code}</code><button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${codeId}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button></pre>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regex fallback when marked/DOMPurify unavailable
|
||||
*/
|
||||
function _formatBasic(text) {
|
||||
let html = esc(text);
|
||||
|
||||
// Code blocks with copy button
|
||||
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
|
||||
const id = 'code-' + Math.random().toString(36).slice(2, 9);
|
||||
return `<pre><code id="${id}" class="language-${lang}">${code.trim()}</code><button class="copy-code-btn" onclick="navigator.clipboard.writeText(document.getElementById('${id}').textContent).then(()=>UI.toast('Copied','success'))">Copy</button></pre>`;
|
||||
});
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
return html;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
|
||||
Reference in New Issue
Block a user