Upload 4 new, 6 modified files from chat-switchboard-v0.5.1.zip

This commit is contained in:
2026-02-18 20:58:13 +00:00
parent 490fe5c6a3
commit 7075538702
8 changed files with 309 additions and 55 deletions

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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

File diff suppressed because one or more lines are too long

3
src/vendor/purify.min.js vendored Normal file

File diff suppressed because one or more lines are too long