mirror of
https://github.com/perstarkse/minne.git
synced 2026-03-18 23:44:18 +01:00
chat history markdown rendering
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user