Add Markdown support
This commit is contained in:
@@ -41,7 +41,9 @@
|
|||||||
"files": {
|
"files": {
|
||||||
"includes": [
|
"includes": [
|
||||||
"internal/service/static/**/*.js",
|
"internal/service/static/**/*.js",
|
||||||
"internal/service/static/**/*.css"
|
"internal/service/static/**/*.css",
|
||||||
|
"!internal/service/static/js/purify-3.4.11.min.js",
|
||||||
|
"!internal/service/static/js/marked-18.0.5.umd.min.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -571,7 +571,6 @@ body {
|
|||||||
|
|
||||||
.message__content {
|
.message__content {
|
||||||
padding: var(--s-2) var(--s-1);
|
padding: var(--s-2) var(--s-1);
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -581,7 +580,6 @@ body {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin-block: var(--s-1);
|
margin-block: var(--s-1);
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,13 +1192,7 @@ body {
|
|||||||
font-size: var(--s-1);
|
font-size: var(--s-1);
|
||||||
color: var(--color-text-muted);
|
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 {
|
.message--tool .message__content {
|
||||||
white-space: normal;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--s-2);
|
gap: var(--s-2);
|
||||||
@@ -1222,7 +1214,6 @@ body {
|
|||||||
font-size: var(--s-1);
|
font-size: var(--s-1);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -1267,3 +1258,128 @@ body {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 $content = $msg.querySelector('.message__content');
|
||||||
const text = extractText(message.content);
|
const text = extractText(message.content);
|
||||||
if (text) {
|
if (text) {
|
||||||
$content.textContent = text;
|
$content.innerHTML = this._renderMarkdown(text);
|
||||||
} else {
|
} else {
|
||||||
$content.remove();
|
$content.remove();
|
||||||
}
|
}
|
||||||
@@ -477,6 +477,39 @@ export class Renderer {
|
|||||||
|
|
||||||
// --- Private ---
|
// --- 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) {
|
_renderTree(messageId) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
this._collectMessages(messageId, messages);
|
this._collectMessages(messageId, messages);
|
||||||
@@ -534,7 +567,7 @@ export class Renderer {
|
|||||||
const $content = $msg.querySelector('.message__content');
|
const $content = $msg.querySelector('.message__content');
|
||||||
const text = extractText(message.content);
|
const text = extractText(message.content);
|
||||||
if (text) {
|
if (text) {
|
||||||
$content.textContent = text;
|
$content.innerHTML = this._renderMarkdown(text);
|
||||||
} else {
|
} else {
|
||||||
$content.remove();
|
$content.remove();
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,15 @@
|
|||||||
<meta name="stt-url" content="{{ .STTURL }}">
|
<meta name="stt-url" content="{{ .STTURL }}">
|
||||||
<meta name="tts-default-voice" content="{{ .TTSDefaultVoice }}">
|
<meta name="tts-default-voice" content="{{ .TTSDefaultVoice }}">
|
||||||
<title>Odidere</title>
|
<title>Odidere</title>
|
||||||
<link rel="stylesheet" href="/static/main.css">
|
<link rel="stylesheet" href="/static/css/main.css">
|
||||||
<script type="module" src="/static/main.js"></script>
|
<!-- 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>
|
</head>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
Reference in New Issue
Block a user