Upload 4 new, 6 modified files from chat-switchboard-v0.5.1.zip
This commit is contained in:
@@ -78,16 +78,71 @@ a { color: var(--accent); }
|
||||
}
|
||||
.message:hover .message-copy { opacity: 0.7; }
|
||||
.message-text { font-size: 0.9rem; line-height: 1.6; word-wrap: break-word; }
|
||||
|
||||
/* ── Markdown Rendered Content ────────────── */
|
||||
|
||||
.message-text p { margin: 0.4em 0; }
|
||||
.message-text p:first-child { margin-top: 0; }
|
||||
.message-text p:last-child { margin-bottom: 0; }
|
||||
|
||||
.message-text pre {
|
||||
background: var(--bg-tertiary); padding: 0.75rem; border-radius: var(--radius);
|
||||
overflow-x: auto; margin: 0.5rem 0; position: relative; font-size: 0.8rem;
|
||||
}
|
||||
.message-text code { font-family: var(--mono); font-size: 0.85em; }
|
||||
.message-text pre code { font-size: inherit; }
|
||||
.message-text code { font-family: var(--mono); font-size: 0.85em; background: var(--bg-tertiary); padding: 0.15em 0.35em; border-radius: 3px; }
|
||||
.message-text pre code { font-size: inherit; background: none; padding: 0; }
|
||||
.copy-code-btn {
|
||||
position: absolute; top: 4px; right: 4px; background: var(--bg-secondary);
|
||||
border: 1px solid var(--border); color: var(--text-secondary); border-radius: 4px;
|
||||
padding: 2px 8px; font-size: 0.7rem; cursor: pointer;
|
||||
padding: 2px 8px; font-size: 0.7rem; cursor: pointer; opacity: 0; transition: opacity 0.15s;
|
||||
}
|
||||
.message-text pre:hover .copy-code-btn { opacity: 1; }
|
||||
.copy-code-btn:hover { background: var(--border); color: var(--text); }
|
||||
|
||||
.message-text ul, .message-text ol { margin: 0.4em 0; padding-left: 1.5em; }
|
||||
.message-text li { margin: 0.2em 0; }
|
||||
.message-text li > p { margin: 0.2em 0; }
|
||||
|
||||
.message-text blockquote {
|
||||
border-left: 3px solid var(--accent); padding: 0.25em 0.75em; margin: 0.4em 0;
|
||||
color: var(--text-secondary); background: var(--bg-tertiary); border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.message-text a { color: var(--accent); text-decoration: none; }
|
||||
.message-text a:hover { text-decoration: underline; }
|
||||
|
||||
.message-text hr { border: none; border-top: 1px solid var(--border); margin: 0.75em 0; }
|
||||
|
||||
.message-text table { border-collapse: collapse; width: 100%; margin: 0.5em 0; font-size: 0.95em; }
|
||||
.message-text th, .message-text td { border: 1px solid var(--border); padding: 0.35em 0.6em; text-align: left; }
|
||||
.message-text th { background: var(--bg-tertiary); font-weight: 600; }
|
||||
|
||||
.message-text img { max-width: 100%; border-radius: 4px; }
|
||||
|
||||
.message-text h1, .message-text h2, .message-text h3,
|
||||
.message-text h4, .message-text h5, .message-text h6 {
|
||||
margin: 0.75em 0 0.35em; font-weight: 600;
|
||||
}
|
||||
.message-text h1 { font-size: 1.3em; }
|
||||
.message-text h2 { font-size: 1.15em; }
|
||||
.message-text h3 { font-size: 1.05em; }
|
||||
|
||||
/* ── Thinking Blocks ─────────────────────── */
|
||||
|
||||
.thinking-block {
|
||||
border: 1px solid rgba(108, 159, 255, 0.15); border-radius: var(--radius);
|
||||
margin: 0.5rem 0; background: rgba(108, 159, 255, 0.04);
|
||||
}
|
||||
.thinking-block summary {
|
||||
padding: 0.4rem 0.75rem; cursor: pointer; font-size: 0.8rem;
|
||||
color: var(--text-secondary); user-select: none;
|
||||
}
|
||||
.thinking-block summary:hover { color: var(--text); }
|
||||
.thinking-block[open] summary { border-bottom: 1px solid rgba(108, 159, 255, 0.1); }
|
||||
.thinking-content {
|
||||
padding: 0.5rem 0.75rem; font-size: 0.8rem; color: var(--text-secondary);
|
||||
max-height: 300px; overflow-y: auto; line-height: 1.5;
|
||||
background: rgba(108, 159, 255, 0.02);
|
||||
}
|
||||
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary); }
|
||||
@@ -96,20 +151,6 @@ a { color: var(--accent); }
|
||||
.error-hint { color: var(--danger); font-size: 0.8rem; padding: 0.5rem; }
|
||||
.loading { color: var(--text-secondary); font-size: 0.8rem; padding: 0.5rem; }
|
||||
|
||||
/* ── Thinking Blocks ─────────────────────── */
|
||||
|
||||
.thinking-block {
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
margin: 0.5rem 0; background: rgba(108, 159, 255, 0.05);
|
||||
}
|
||||
.thinking-block summary {
|
||||
padding: 0.4rem 0.75rem; cursor: pointer; font-size: 0.8rem; color: var(--text-secondary);
|
||||
}
|
||||
.thinking-content {
|
||||
padding: 0.5rem 0.75rem; font-size: 0.8rem; color: var(--text-secondary);
|
||||
border-top: 1px solid var(--border); max-height: 300px; overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Input Area ──────────────────────────── */
|
||||
|
||||
.input-area {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="favicon-192.png">
|
||||
<link rel="stylesheet" href="css/styles.css?v=0.5.0">
|
||||
<link rel="stylesheet" href="css/styles.css?v=0.5.1">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -297,9 +297,17 @@
|
||||
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script src="js/debug.js?v=0.5.0"></script>
|
||||
<script src="js/api.js?v=0.5.0"></script>
|
||||
<script src="js/ui.js?v=0.5.0"></script>
|
||||
<script src="js/app.js?v=0.5.0"></script>
|
||||
<!-- Markdown: local vendor first, CDN fallback for connected envs -->
|
||||
<script src="vendor/marked.min.js"
|
||||
onerror="this.onerror=null; this.src='https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.min.js'">
|
||||
</script>
|
||||
<script src="vendor/purify.min.js"
|
||||
onerror="this.onerror=null; this.src='https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js'">
|
||||
</script>
|
||||
|
||||
<script src="js/debug.js?v=0.5.1"></script>
|
||||
<script src="js/api.js?v=0.5.1"></script>
|
||||
<script src="js/ui.js?v=0.5.1"></script>
|
||||
<script src="js/app.js?v=0.5.1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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) {
|
||||
|
||||
74
src/vendor/marked.min.js
vendored
Normal file
74
src/vendor/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
src/vendor/purify.min.js
vendored
Normal file
3
src/vendor/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user