chat history markdown rendering

This commit is contained in:
Per Stark
2025-04-10 11:54:22 +02:00
parent 9645df4999
commit 9b538098cd
3 changed files with 229 additions and 83 deletions

View File

@@ -1,10 +1,10 @@
<div id="chat_container" class="pl-3 overflow-y-auto h-[calc(100vh-175px)] hide-scrollbar">
<div id="chat_container" class="pl-3 overflow-y-auto h-[calc(100vh-155px)] hide-scrollbar">
{% for message in history %}
{% if message.role == "AI" %}
<div class="chat chat-start">
<div>
<div class="chat-bubble">
{{ message.content }}
<div id="{{message.id}}" class="chat-bubble markdown-content">
{{ message.content|safe }}
</div>
{% if message.references %}
{% include "chat/reference_list.html" %}
@@ -13,42 +13,130 @@
</div>
{% else %}
<div class="chat chat-end">
<div class="chat-bubble">
{{ message.content }}
<div id="{{message.id}}" class="chat-bubble markdown-content">
{{ message.content|safe }}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.body.addEventListener('htmx:afterSwap', function (evt) {
const chatContainer = document.getElementById('chat_container');
if (chatContainer) {
setTimeout(() => {
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 0);
}
// Configure marked options
marked.setOptions({
breaks: true, // Critical for chat - preserves line breaks
gfm: true, // Enables GitHub Flavored Markdown features
headerIds: false, // Prevents ID collisions in dynamic chat content
mangle: false, // Safer for dynamic content
smartLists: true, // Better list rendering
smartypants: true, // Makes quotes and dashes look nicer
xhtml: false // No need for self-closing tags in most chat UIs
});
window.addEventListener('load', function () {
// Process all markdown content elements
function renderMarkdown() {
document.querySelectorAll('.markdown-content').forEach(el => {
const rawContent = el.getAttribute('data-content') || el.textContent;
el.innerHTML = marked.parse(rawContent);
});
}
// Add data attributes for raw content on server side
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.markdown-content').forEach(el => {
el.setAttribute('data-content', el.textContent.trim());
});
renderMarkdown();
scrollChatToBottom();
});
function scrollChatToBottom() {
const chatContainer = document.getElementById('chat_container');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}
document.body.addEventListener('htmx:afterSwap', function (evt) {
renderMarkdown();
setTimeout(scrollChatToBottom, 0);
});
</script>
<style>
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
/* Chrome, Safari and Opera */
}
.markdown-content p {
margin-bottom: 0.75em;
}
.markdown-content p:last-child {
margin-bottom: 0;
}
.markdown-content ul,
.markdown-content ol {
margin-top: 0.5em;
margin-bottom: 0.75em;
padding-left: 2em;
}
.markdown-content li {
margin-bottom: 0.25em;
}
.markdown-content pre {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.5em;
border-radius: 4px;
overflow-x: auto;
}
.markdown-content code {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
.markdown-content {
line-height: 1.5;
word-wrap: break-word;
}
/* Better table support */
.markdown-content table {
border-collapse: collapse;
margin: 0.75em 0;
width: 100%;
}
.markdown-content th,
.markdown-content td {
border: 1px solid #ddd;
padding: 6px 12px;
text-align: left;
}
/* Better blockquote for chat */
.markdown-content blockquote {
border-left: 4px solid #ddd;
padding-left: 10px;
margin: 0.5em 0 0.5em 0.5em;
color: #666;
}
/* Horizontal rule */
.markdown-content hr {
border: none;
border-top: 1px solid #ddd;
margin: 0.75em 0;
}
</style>

View File

@@ -1,11 +1,12 @@
<div class="relative my-2">
<button id="references-toggle-{{message.id}}"
class="text-xs text-blue-500 hover:text-blue-700 hover:underline focus:outline-none flex items-center">
class="text-xs text-blue-500 hover:text-blue-700 hover:underline focus:outline-none flex items-center"
onclick="toggleReferences('{{message.id}}')">
References
{% include "icons/chevron_icon.html" %}
</button>
<div id="references-content-{{message.id}}" class="hidden max-w-full mt-1">
<div class="flex flex-wrap gap-1">
<div id="references-list-{{message.id}}" class="flex flex-wrap gap-1">
{% for reference in message.references %}
<div class="reference-badge-container" data-reference="{{reference}}" data-message-id="{{message.id}}"
data-index="{{loop.index}}">
@@ -19,72 +20,130 @@
</div>
<script>
document.getElementById('references-toggle-{{message.id}}').addEventListener('click', function () {
const content = document.getElementById('references-content-{{message.id}}');
const icon = document.getElementById('toggle-icon');
content.classList.toggle('hidden');
icon.classList.toggle('rotate-180');
});
function toggleReferences(messageId) {
const refsContent = document.getElementById('references-content-' + messageId);
const refsList = document.getElementById('references-list-' + messageId);
const toggleBtn = document.getElementById('references-toggle-' + messageId);
// Initialize portal tooltips
document.querySelectorAll('.reference-badge-container').forEach(container => {
const reference = container.dataset.reference;
const messageId = container.dataset.messageId;
const index = container.dataset.index;
let tooltipId = `tooltip-${messageId}-${index}`;
let tooltipContent = null;
let tooltipTimeout;
// Toggle visibility
if (refsContent.classList.contains('hidden')) {
refsContent.classList.remove('hidden');
// Create tooltip element (initially hidden)
function createTooltip() {
const tooltip = document.createElement('div');
tooltip.id = tooltipId;
tooltip.className = 'fixed z-[9999] bg-neutral-800 text-white p-3 rounded-md shadow-lg text-sm w-72 max-w-xs border border-neutral-700 hidden';
tooltip.innerHTML = '<div class="animate-pulse">Loading...</div>';
document.body.appendChild(tooltip);
return tooltip;
// Wait for DOM update then scroll to make visible
setTimeout(() => {
refsList.scrollIntoView({behavior: 'smooth', block: 'nearest'});
// Also ensure chat container updates its scroll position
const chatContainer = document.getElementById('chat_container');
if (chatContainer) {
const refPosition = refsList.getBoundingClientRect().bottom;
const containerBottom = chatContainer.getBoundingClientRect().bottom;
if (refPosition > containerBottom) {
chatContainer.scrollTop += (refPosition - containerBottom + 20);
}
}
}, 50);
} else {
refsContent.classList.add('hidden');
}
container.addEventListener('mouseenter', function () {
// Clear any existing timeout
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// Rotate chevron icon (assuming it's the first SVG in the button)
const chevron = toggleBtn.querySelector('svg');
if (chevron) {
chevron.style.transform = refsContent.classList.contains('hidden') ?
'rotate(0deg)' : 'rotate(180deg)';
}
}
// Get or create tooltip
let tooltip = document.getElementById(tooltipId);
if (!tooltip) tooltip = createTooltip();
// Position tooltip
const rect = container.getBoundingClientRect();
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
tooltip.style.left = `${rect.left + window.scrollX}px`;
// Adjust position if it would overflow viewport
const tooltipRect = tooltip.getBoundingClientRect();
if (rect.left + tooltipRect.width > window.innerWidth - 20) {
tooltip.style.left = `${window.innerWidth - tooltipRect.width - 20 + window.scrollX}px`;
}
// Show tooltip
tooltip.classList.remove('hidden');
// Load content if needed
if (!tooltipContent) {
fetch(`/chat/reference/${encodeURIComponent(reference)}`)
.then(response => response.text())
.then(html => {
tooltipContent = html;
if (document.getElementById(tooltipId)) {
document.getElementById(tooltipId).innerHTML = html;
}
});
}
});
container.addEventListener('mouseleave', function () {
tooltipTimeout = setTimeout(() => {
const tooltip = document.getElementById(tooltipId);
if (tooltip) tooltip.classList.add('hidden');
}, 200);
});
// Initialize portal tooltips
document.addEventListener('DOMContentLoaded', function () {
initializeReferenceTooltips();
});
</script>
document.body.addEventListener('htmx:afterSwap', function () {
initializeReferenceTooltips();
});
function initializeReferenceTooltips() {
document.querySelectorAll('.reference-badge-container').forEach(container => {
if (container.dataset.initialized === 'true') return;
const reference = container.dataset.reference;
const messageId = container.dataset.messageId;
const index = container.dataset.index;
let tooltipId = `tooltip-${messageId}-${index}`;
let tooltipContent = null;
let tooltipTimeout;
// Create tooltip element (initially hidden)
function createTooltip() {
const tooltip = document.createElement('div');
tooltip.id = tooltipId;
tooltip.className = 'fixed z-[9999] bg-neutral-800 text-white p-3 rounded-md shadow-lg text-sm w-72 max-w-xs border border-neutral-700 hidden';
tooltip.innerHTML = '<div class="animate-pulse">Loading...</div>';
document.body.appendChild(tooltip);
return tooltip;
}
container.addEventListener('mouseenter', function () {
// Clear any existing timeout
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// Get or create tooltip
let tooltip = document.getElementById(tooltipId);
if (!tooltip) tooltip = createTooltip();
// Position tooltip
const rect = container.getBoundingClientRect();
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
tooltip.style.left = `${rect.left + window.scrollX}px`;
// Adjust position if it would overflow viewport
const tooltipRect = tooltip.getBoundingClientRect();
if (rect.left + tooltipRect.width > window.innerWidth - 20) {
tooltip.style.left = `${window.innerWidth - tooltipRect.width - 20 + window.scrollX}px`;
}
// Show tooltip
tooltip.classList.remove('hidden');
// Load content if needed
if (!tooltipContent) {
fetch(`/chat/reference/${encodeURIComponent(reference)}`)
.then(response => response.text())
.then(html => {
tooltipContent = html;
if (document.getElementById(tooltipId)) {
document.getElementById(tooltipId).innerHTML = html;
}
});
} else if (tooltip) {
// Set content if already loaded
tooltip.innerHTML = tooltipContent;
}
});
container.addEventListener('mouseleave', function () {
tooltipTimeout = setTimeout(() => {
const tooltip = document.getElementById(tooltipId);
if (tooltip) tooltip.classList.add('hidden');
}, 200);
});
container.dataset.initialized = 'true';
});
}
</script>
<style>
#references-toggle- {
{
message.id
}
}
svg {
transition: transform 0.2s ease;
}
</style>

View File

@@ -19,7 +19,6 @@
<script src="/assets/htmx-ext-sse.js" defer></script>
<script src="/assets/theme-toggle.js" defer></script>
<script src="/assets/toast.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Icons -->
<link rel="icon" href="/assets/icon/favicon.ico">