feat(web): render Lyra's replies as Markdown (readable, not a wall of asterisks)
Her replies are full of **bold**, numbered lists and headings but rendered as raw monospace text, so the chat was a cluttered wall of literal markup. Add a small self-contained Markdown renderer (no deps): headings, ordered/unordered lists, bold/italic, inline + fenced code, links + autolinked URLs, with HTML escaping. Assistant messages now render to HTML; user/system stay literal text. Proportional font + spacing/list/code styling for assistant bubbles. (Renderer avoids literal backticks via String.fromCharCode(96) — a triple-tick regex literal had been corrupting the file with NUL bytes.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -298,12 +298,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
var bt = String.fromCharCode(96);
|
||||
var esc = function (s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); };
|
||||
var src = String(text == null ? "" : text).replace(/\r\n/g, "\n");
|
||||
var blocks = [];
|
||||
var fenceRe = new RegExp(bt + bt + bt + "[^\\n]*\\n?([\\s\\S]*?)" + bt + bt + bt, "g");
|
||||
src = src.replace(fenceRe, function (_, code) { blocks.push(code.replace(/\n+$/, "")); return "@@CB" + (blocks.length - 1) + "@@"; });
|
||||
var codeRe = new RegExp(bt + "([^" + bt + "]+)" + bt, "g");
|
||||
var inline = function (s) {
|
||||
return esc(s)
|
||||
.replace(codeRe, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
||||
.replace(/\*([^*\n]+)\*/g, "<em>$1</em>")
|
||||
.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||||
.replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g, '$1<a href="$2" target="_blank" rel="noopener">$2</a>');
|
||||
};
|
||||
var lines = src.split("\n");
|
||||
var out = [], para = [], list = null;
|
||||
var flushPara = function () { if (para.length) { out.push("<p>" + para.map(inline).join("<br>") + "</p>"); para = []; } };
|
||||
var flushList = function () { if (list) { out.push("<" + list.t + ">" + list.items.map(function (it) { return "<li>" + inline(it) + "</li>"; }).join("") + "</" + list.t + ">"); list = null; } };
|
||||
var flushAll = function () { flushPara(); flushList(); };
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].replace(/\s+$/, ""); var t = line.trim(); var m;
|
||||
if ((m = t.match(/^@@CB(\d+)@@$/))) { flushAll(); out.push("<pre><code>" + esc(blocks[+m[1]]) + "</code></pre>"); continue; }
|
||||
if (!t) { flushAll(); continue; }
|
||||
if ((m = line.match(/^(#{1,4})\s+(.*)$/))) { flushAll(); out.push("<h" + m[1].length + ">" + inline(m[2]) + "</h" + m[1].length + ">"); continue; }
|
||||
if ((m = line.match(/^\s*\d+[.)]\s+(.*)$/))) { flushPara(); if (!list || list.t !== "ol") { flushList(); list = { t: "ol", items: [] }; } list.items.push(m[1]); continue; }
|
||||
if ((m = line.match(/^\s*[-*+]\s+(.*)$/))) { flushPara(); if (!list || list.t !== "ul") { flushList(); list = { t: "ul", items: [] }; } list.items.push(m[1]); continue; }
|
||||
flushList(); para.push(line);
|
||||
}
|
||||
flushAll();
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function addMessage(role, text, autoScroll = true) {
|
||||
const messagesEl = document.getElementById("messages");
|
||||
|
||||
const msgDiv = document.createElement("div");
|
||||
msgDiv.className = `msg ${role}`;
|
||||
msgDiv.textContent = text;
|
||||
if (role === "assistant") { msgDiv.innerHTML = renderMarkdown(text); } else { msgDiv.textContent = text; }
|
||||
messagesEl.appendChild(msgDiv);
|
||||
|
||||
// Auto-scroll to bottom if enabled
|
||||
|
||||
+87
-56
@@ -907,59 +907,90 @@ select:hover {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Live Log lines ---- */
|
||||
.log-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
border-left: 3px solid var(--text-fade);
|
||||
animation: thinkingSlideIn 0.25s ease-out;
|
||||
word-break: break-word;
|
||||
}
|
||||
.log-time { color: var(--text-fade); flex-shrink: 0; }
|
||||
.log-level {
|
||||
flex-shrink: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.log-msg { color: var(--text); }
|
||||
.log-fields { color: var(--text-fade); width: 100%; padding-left: 4px; }
|
||||
|
||||
.log-info { border-left-color: #00bfff; }
|
||||
.log-info .log-level { color: #7dd3fc; }
|
||||
.log-debug { border-left-color: #8a2be2; }
|
||||
.log-debug .log-level { color: #c79cff; }
|
||||
.log-error { border-left-color: #ff3333; background: rgba(255,51,51,0.08); }
|
||||
.log-error .log-level, .log-error .log-msg { color: #fca5a5; }
|
||||
.log-system { border-left-color: #00ff66; }
|
||||
.log-system .log-level { color: #00ff66; }
|
||||
|
||||
.log-detail { width: 100%; margin-top: 4px; }
|
||||
.log-detail summary {
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
font-size: 0.72rem;
|
||||
user-select: none;
|
||||
}
|
||||
.log-detail pre {
|
||||
margin: 6px 0 0;
|
||||
padding: 8px;
|
||||
max-height: 340px;
|
||||
overflow: auto;
|
||||
background: rgba(0,0,0,0.25);
|
||||
border-left: 2px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---- Live Log lines ---- */
|
||||
.log-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
border-left: 3px solid var(--text-fade);
|
||||
animation: thinkingSlideIn 0.25s ease-out;
|
||||
word-break: break-word;
|
||||
}
|
||||
.log-time { color: var(--text-fade); flex-shrink: 0; }
|
||||
.log-level {
|
||||
flex-shrink: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.log-msg { color: var(--text); }
|
||||
.log-fields { color: var(--text-fade); width: 100%; padding-left: 4px; }
|
||||
|
||||
.log-info { border-left-color: #00bfff; }
|
||||
.log-info .log-level { color: #7dd3fc; }
|
||||
.log-debug { border-left-color: #8a2be2; }
|
||||
.log-debug .log-level { color: #c79cff; }
|
||||
.log-error { border-left-color: #ff3333; background: rgba(255,51,51,0.08); }
|
||||
.log-error .log-level, .log-error .log-msg { color: #fca5a5; }
|
||||
.log-system { border-left-color: #00ff66; }
|
||||
.log-system .log-level { color: #00ff66; }
|
||||
|
||||
.log-detail { width: 100%; margin-top: 4px; }
|
||||
.log-detail summary {
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
font-size: 0.72rem;
|
||||
user-select: none;
|
||||
}
|
||||
.log-detail pre {
|
||||
margin: 6px 0 0;
|
||||
padding: 8px;
|
||||
max-height: 340px;
|
||||
overflow: auto;
|
||||
background: rgba(0,0,0,0.25);
|
||||
border-left: 2px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Rendered markdown in Lyra's replies — readable proportional type + structure. */
|
||||
.msg.assistant {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.55;
|
||||
max-width: 88%;
|
||||
}
|
||||
.msg.assistant p { margin: 0 0 10px; }
|
||||
.msg.assistant p:last-child { margin-bottom: 0; }
|
||||
.msg.assistant h1, .msg.assistant h2, .msg.assistant h3, .msg.assistant h4 {
|
||||
margin: 14px 0 6px; line-height: 1.3; color: var(--accent);
|
||||
}
|
||||
.msg.assistant h1 { font-size: 1.18rem; }
|
||||
.msg.assistant h2 { font-size: 1.1rem; }
|
||||
.msg.assistant h3 { font-size: 1.02rem; }
|
||||
.msg.assistant h4 { font-size: 0.96rem; }
|
||||
.msg.assistant ul, .msg.assistant ol { margin: 6px 0 10px; padding-left: 22px; }
|
||||
.msg.assistant li { margin: 3px 0; }
|
||||
.msg.assistant li > ul, .msg.assistant li > ol { margin: 3px 0; }
|
||||
.msg.assistant strong { font-weight: 600; color: var(--text); }
|
||||
.msg.assistant em { font-style: italic; }
|
||||
.msg.assistant a { color: var(--accent); text-decoration: underline; }
|
||||
.msg.assistant code {
|
||||
font-family: "IBM Plex Mono", monospace; font-size: 0.88em;
|
||||
background: rgba(255,255,255,0.08); padding: 1px 5px; border-radius: 4px;
|
||||
}
|
||||
.msg.assistant pre {
|
||||
background: rgba(0,0,0,0.32); border: 1px solid rgba(255,102,0,0.3);
|
||||
border-radius: 6px; padding: 10px 12px; margin: 8px 0; overflow-x: auto;
|
||||
}
|
||||
.msg.assistant pre code { background: none; padding: 0; font-size: 0.85em; }
|
||||
|
||||
Reference in New Issue
Block a user