[Developer] #424 render VPD LLM answer as markdown
This commit is contained in:
@@ -550,7 +550,8 @@ body {
|
||||
padding: .6rem .8rem;
|
||||
}
|
||||
|
||||
.ai-answer pre {
|
||||
.ai-answer pre,
|
||||
.markdown-view {
|
||||
color: var(--rw-text);
|
||||
font-family: inherit;
|
||||
font-size: .95rem;
|
||||
@@ -559,6 +560,74 @@ body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.markdown-view {
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-view h2,
|
||||
.markdown-view h3 {
|
||||
border: 0;
|
||||
color: var(--rw-text);
|
||||
margin: .9rem 0 .45rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-view h2:first-child,
|
||||
.markdown-view h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-view h2 {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.markdown-view h3 {
|
||||
font-size: .98rem;
|
||||
}
|
||||
|
||||
.markdown-view p {
|
||||
margin: .45rem 0;
|
||||
}
|
||||
|
||||
.markdown-view ul,
|
||||
.markdown-view ol {
|
||||
margin: .45rem 0 .65rem 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-view code {
|
||||
background: rgba(49, 45, 42, .08);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 4px;
|
||||
color: var(--rw-text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: .88em;
|
||||
padding: .08rem .28rem;
|
||||
}
|
||||
|
||||
.markdown-view table {
|
||||
background: var(--rw-surface);
|
||||
border-collapse: collapse;
|
||||
font-size: .86rem;
|
||||
margin: .7rem 0;
|
||||
min-width: 760px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-view th,
|
||||
.markdown-view td {
|
||||
border: 1px solid var(--rw-border);
|
||||
padding: .55rem .65rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.markdown-view th {
|
||||
background: var(--rw-surface-muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.form-grid .span-2 {
|
||||
grid-column: span 1;
|
||||
|
||||
@@ -5,6 +5,125 @@ document.body.addEventListener('htmx:responseError', (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function renderInlineMarkdown(value) {
|
||||
return escapeHtml(value)
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
function isMarkdownTable(lines, index) {
|
||||
if (index + 1 >= lines.length) {
|
||||
return false;
|
||||
}
|
||||
return lines[index].trim().startsWith('|')
|
||||
&& lines[index + 1].trim().startsWith('|')
|
||||
&& /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(lines[index + 1].trim());
|
||||
}
|
||||
|
||||
function splitMarkdownTableRow(line) {
|
||||
return line.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim());
|
||||
}
|
||||
|
||||
function renderMarkdownTable(lines, start) {
|
||||
const headers = splitMarkdownTableRow(lines[start]);
|
||||
let cursor = start + 2;
|
||||
const rows = [];
|
||||
while (cursor < lines.length && lines[cursor].trim().startsWith('|')) {
|
||||
rows.push(splitMarkdownTableRow(lines[cursor]));
|
||||
cursor += 1;
|
||||
}
|
||||
const headerHtml = headers.map((cell) => `<th>${renderInlineMarkdown(cell)}</th>`).join('');
|
||||
const bodyHtml = rows.map((row) => {
|
||||
const cells = headers.map((_, index) => `<td>${renderInlineMarkdown(row[index] || '')}</td>`).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
return {
|
||||
html: `<table><thead><tr>${headerHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`,
|
||||
next: cursor
|
||||
};
|
||||
}
|
||||
|
||||
function renderMarkdownText(markdown) {
|
||||
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
||||
const html = [];
|
||||
let listType = null;
|
||||
const closeList = () => {
|
||||
if (listType) {
|
||||
html.push(`</${listType}>`);
|
||||
listType = null;
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
closeList();
|
||||
continue;
|
||||
}
|
||||
if (isMarkdownTable(lines, i)) {
|
||||
closeList();
|
||||
const table = renderMarkdownTable(lines, i);
|
||||
html.push(table.html);
|
||||
i = table.next - 1;
|
||||
continue;
|
||||
}
|
||||
const heading = trimmed.match(/^(#{2,4})\s+(.+)$/);
|
||||
if (heading) {
|
||||
closeList();
|
||||
const level = Math.min(heading[1].length, 3);
|
||||
html.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
const unordered = trimmed.match(/^[-*]\s+(.+)$/);
|
||||
if (unordered) {
|
||||
if (listType !== 'ul') {
|
||||
closeList();
|
||||
listType = 'ul';
|
||||
html.push('<ul>');
|
||||
}
|
||||
html.push(`<li>${renderInlineMarkdown(unordered[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
const ordered = trimmed.match(/^\d+\.\s+(.+)$/);
|
||||
if (ordered) {
|
||||
if (listType !== 'ol') {
|
||||
closeList();
|
||||
listType = 'ol';
|
||||
html.push('<ol>');
|
||||
}
|
||||
html.push(`<li>${renderInlineMarkdown(ordered[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
closeList();
|
||||
html.push(`<p>${renderInlineMarkdown(trimmed)}</p>`);
|
||||
}
|
||||
closeList();
|
||||
return html.join('');
|
||||
}
|
||||
|
||||
function renderMarkdownViews(root = document) {
|
||||
root.querySelectorAll('[data-markdown-view]').forEach((view) => {
|
||||
if (view.dataset.rendered === 'true') {
|
||||
return;
|
||||
}
|
||||
view.innerHTML = renderMarkdownText(view.textContent || '');
|
||||
view.dataset.rendered = 'true';
|
||||
});
|
||||
}
|
||||
|
||||
function filterUserRoleDetail() {
|
||||
const master = document.getElementById('userRoleMaster');
|
||||
const table = document.getElementById('userRoleDetail');
|
||||
@@ -98,6 +217,7 @@ async function syncObjectCatalogSelection() {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
renderMarkdownViews();
|
||||
const master = document.getElementById('userRoleMaster');
|
||||
if (master) {
|
||||
master.addEventListener('change', filterUserRoleDetail);
|
||||
@@ -129,3 +249,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
renderMarkdownViews(event.detail.target || document);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<section class="ai-answer">
|
||||
<h3>Answer</h3>
|
||||
<pre th:text="${explanation.answer()}">answer</pre>
|
||||
<h3>분석 결과</h3>
|
||||
<div class="markdown-view" data-markdown-view th:text="${explanation.answer()}">answer</div>
|
||||
</section>
|
||||
<div class="probe-exchange-grid">
|
||||
<section class="probe-exchange">
|
||||
|
||||
Reference in New Issue
Block a user