Add Markdown support
This commit is contained in:
@@ -571,7 +571,6 @@ body {
|
||||
|
||||
.message__content {
|
||||
padding: var(--s-2) var(--s-1);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -581,7 +580,6 @@ body {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-block: var(--s-1);
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
@@ -1194,13 +1192,7 @@ body {
|
||||
font-size: var(--s-1);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* .message__content inherits white-space: pre-wrap for plain text messages
|
||||
(to preserve newlines from LLM output). For tool messages, the content is
|
||||
structured HTML with newlines between child elements; pre-wrap renders those
|
||||
as visible blank lines, causing large vertical gaps. Reset to normal. */
|
||||
.message--tool .message__content {
|
||||
white-space: normal;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2);
|
||||
@@ -1222,7 +1214,6 @@ body {
|
||||
font-size: var(--s-1);
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius);
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -1267,3 +1258,128 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- Markdown rendering --- */
|
||||
|
||||
.message__content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message__content p + p {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.message__content pre {
|
||||
background: color-mix(in srgb, var(--color-gray1, #1a1a2e) 100%, white 5%);
|
||||
border-radius: var(--radius, 6px);
|
||||
padding: 0.75em 1em;
|
||||
margin-block: 0.75em;
|
||||
}
|
||||
|
||||
.message__content code {
|
||||
font-family: var(--font-mono, "SF Mono", Menlo, Consolas, monospace);
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
.message__content :not(pre) > code {
|
||||
background: color-mix(in srgb, var(--color-gray1, #1a1a2e) 100%, white 8%);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message__content blockquote {
|
||||
margin-block: 0.75em;
|
||||
padding: 0 0.75em;
|
||||
border-left: 2px solid var(--color-border, #333);
|
||||
color: var(--color-text-muted, #888);
|
||||
}
|
||||
|
||||
.message__content ul,
|
||||
.message__content ol {
|
||||
margin-block: 0.75em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.message__content li + li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message__content li > ul,
|
||||
.message__content li > ol {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message__content dl {
|
||||
margin-block: 0.75em;
|
||||
}
|
||||
|
||||
.message__content dt {
|
||||
font-weight: 600;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.message__content dt + dt {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message__content dd {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 0.125em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message__content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-block: 0.75em;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.message__content th,
|
||||
.message__content td {
|
||||
border: 1px solid var(--color-border-light, #333);
|
||||
padding: 0.4em 0.6em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message__content th {
|
||||
background: var(--color-gray1, #1a1a2e);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message__table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.message__content a {
|
||||
color: var(--color-accent, #5b9bd5);
|
||||
}
|
||||
|
||||
.message__content hr {
|
||||
border: none;
|
||||
border-top: 2px solid var(--color-border-light, #333);
|
||||
margin-block: 1em;
|
||||
}
|
||||
|
||||
.message__content h1,
|
||||
.message__content h2,
|
||||
.message__content h3,
|
||||
.message__content h4,
|
||||
.message__content h5,
|
||||
.message__content h6 {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.25em;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.message__content h1 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.message__content h2 {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
.message__content h3 {
|
||||
font-size: 1.05em;
|
||||
}
|
||||
8
internal/service/static/js/marked-18.0.5.umd.min.js
vendored
Normal file
8
internal/service/static/js/marked-18.0.5.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
internal/service/static/js/purify-3.4.11.min.js
vendored
Normal file
3
internal/service/static/js/purify-3.4.11.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -331,7 +331,7 @@ export class Renderer {
|
||||
const $content = $msg.querySelector('.message__content');
|
||||
const text = extractText(message.content);
|
||||
if (text) {
|
||||
$content.textContent = text;
|
||||
$content.innerHTML = this._renderMarkdown(text);
|
||||
} else {
|
||||
$content.remove();
|
||||
}
|
||||
@@ -477,6 +477,39 @@ export class Renderer {
|
||||
|
||||
// --- Private ---
|
||||
|
||||
/**
|
||||
* _renderMarkdown converts Markdown text to safe HTML.
|
||||
* Uses marked for parsing and DOMPurify for sanitization.
|
||||
* Falls back to plain text if libraries are unavailable.
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
_renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
try {
|
||||
if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
|
||||
const html = marked.parse(text);
|
||||
// Wrap tables in a scrollable container for mobile.
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
for (const table of container.querySelectorAll('table')) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'message__table-scroll';
|
||||
table.replaceWith(wrapper);
|
||||
wrapper.appendChild(table);
|
||||
}
|
||||
return DOMPurify.sanitize(container.innerHTML);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('failed to render markdown:', e);
|
||||
}
|
||||
// Fallback: escape and return plain text
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
div.style.whiteSpace = 'pre-wrap';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
_renderTree(messageId) {
|
||||
const messages = [];
|
||||
this._collectMessages(messageId, messages);
|
||||
@@ -534,7 +567,7 @@ export class Renderer {
|
||||
const $content = $msg.querySelector('.message__content');
|
||||
const text = extractText(message.content);
|
||||
if (text) {
|
||||
$content.textContent = text;
|
||||
$content.innerHTML = this._renderMarkdown(text);
|
||||
} else {
|
||||
$content.remove();
|
||||
}
|
||||
@@ -8,7 +8,15 @@
|
||||
<meta name="stt-url" content="{{ .STTURL }}">
|
||||
<meta name="tts-default-voice" content="{{ .TTSDefaultVoice }}">
|
||||
<title>Odidere</title>
|
||||
<link rel="stylesheet" href="/static/main.css">
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
<!-- marked: Markdown parser. Source: https://github.com/markedjs/marked -->
|
||||
<script src="/static/js/marked-18.0.5.umd.min.js"
|
||||
integrity="sha384-MU0bL51BF1u980ujv+Pk0Z0qCjhzfBIEHIFPkUq81289nkiAr+q1jSxEkdlIeHLJ"
|
||||
crossorigin="anonymous"></script>
|
||||
<!-- DOMPurify: HTML sanitizer. Source: https://github.com/cure53/DOMPurify -->
|
||||
<script src="/static/js/purify-3.4.11.min.js"
|
||||
integrity="sha384-o44XUELLEnv/iSlA1NWxBweqbD4TSR0qgq2VzVsxtkHS989JJjGKSE9vkfo5MN4K"
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="module" src="/static/js/main.js"></script>
|
||||
</head>
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user