feat: markdown formatting complete

streaming and history implemented
This commit is contained in:
Per Stark
2025-04-11 15:18:55 +02:00
parent 9b538098cd
commit 62f2bddb8f
3 changed files with 36 additions and 51 deletions
+1 -3
View File
@@ -57,10 +57,8 @@
chatContainer.scrollTop = chatContainer.scrollHeight; chatContainer.scrollTop = chatContainer.scrollHeight;
} }
} }
document.body.addEventListener('htmx:afterSwap', function (evt) { document.body.addEventListener('htmx:afterSwap', function (evt) {
renderMarkdown(); scrollChatToBottom();
setTimeout(scrollChatToBottom, 0);
}); });
</script> </script>
<style> <style>
@@ -1,60 +1,47 @@
<div class="chat chat-end"> <div class="chat chat-end">
<div class="chat-bubble"> <div class="chat-bubble markdown-content">
{{user_message.content}} {{ user_message.content }}
</div> </div>
</div> </div>
<div class="chat chat-start"> <div class="chat chat-start">
<div hx-ext="sse" sse-connect="/chat/response-stream?message_id={{user_message.id}}" sse-close="close_stream" <div hx-ext="sse" sse-connect="/chat/response-stream?message_id={{user_message.id}}" sse-close="close_stream"
hx-swap="beforeend"> hx-swap="beforeend">
<div class="chat-bubble" sse-swap="chat_message"> <div class="chat-bubble">
<span class="loading loading-dots loading-sm loading-id-{{user_message.id}}"></span> <span class="loading loading-dots loading-sm loading-id-{{user_message.id}}"></span>
</div> <div class="markdown-content" id="ai-message-content-{{user_message.id}}" sse-swap="chat_message"></div>
<div class="chat-footer opacity-50 max-w-[90%] flex-wrap" sse-swap="references">
</div> </div>
</div> </div>
</div> </div>
<script> <script>
document.body.addEventListener('htmx:sseBeforeMessage', (e) => { marked.setOptions({
const targetElement = e.detail.elt; breaks: true, gfm: true, headerIds: false, mangle: false
const loadingSpinner = targetElement.querySelector('.loading-id-{{user_message.id}}');
// Hiding the loading spinner before data is swapped in
if (loadingSpinner) {
loadingSpinner.style.display = 'none';
}
}
)
// Add listener for after content is settled
document.body.addEventListener('htmx:afterSettle', function (evt) {
// Check if the settled element has our specific class
// evt.detail.target might be the container, elt is often the element *making* the request
// We need the element *receiving* the swap. Let's target specifically.
const messageId = "{{user_message.id}}"; // Get the ID from the template context
const targetBubble = document.querySelector(`.ai-message-content-${messageId}`);
// Ensure we have the marked library and the target exists
if (targetBubble && typeof marked !== 'undefined') {
// Get the raw text content (which includes previously streamed parts)
// Exclude the spinner if it's still somehow there, though it should be hidden.
let rawContent = '';
targetBubble.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
rawContent += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('loading')) {
// In case HTMX wraps text in spans or something unexpected later
rawContent += node.textContent;
}
}); });
console.log(rawContent); // Buffer store for markdown, keyed by message id
// Sanitize BEFORE inserting potentially harmful HTML from Markdown window.markdownBuffer = window.markdownBuffer || {};
// It's better to sanitize *after* rendering if using DOMPurify document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
targetBubble.innerHTML = marked.parse(rawContent); const spinner = document.querySelector('.loading-id-{{user_message.id}}');
// Optional: Sanitize with DOMPurify *after* parsing for security if (spinner) spinner.style.display = 'none';
// if (typeof DOMPurify !== 'undefined') {
// targetBubble.innerHTML = DOMPurify.sanitize(marked.parse(rawContent)); const el = document.getElementById('ai-message-content-{{user_message.id}}');
// } else { if (e.detail.elt !== el) return;
// targetBubble.innerHTML = marked.parse(rawContent); // Use with caution if markdown source isn't trusted e.preventDefault(); // Prevent htmx from swapping
// } // Use message id as buffer key
const msgId = '{{user_message.id}}';
window.markdownBuffer[msgId] = (window.markdownBuffer[msgId] || '') + (e.detail.data || '');
// Render buffer (with newline fix) on *every* chunk
el.innerHTML = marked.parse(
window.markdownBuffer[msgId].replace(/\\n/g, '\n')
);
scrollChatToBottom();
});
document.body.addEventListener('htmx:sseClose', function () {
const el = document.getElementById('ai-message-content-{{user_message.id}}');
const msgId = '{{user_message.id}}';
if (el && window.markdownBuffer[msgId]) {
el.innerHTML = marked.parse(
window.markdownBuffer[msgId].replace(/\\n/g, '\n')
);
delete window.markdownBuffer[msgId]; // Clean up in multichat
} }
}); });
</script> </script>
+3 -3
View File
@@ -1,8 +1,5 @@
\[\] archive ingressed webpage \[\] archive ingressed webpage
\[x\] configs primarily get envs
\[\] filtering on categories \[\] filtering on categories
\[\] markdown rendering in client
\[x\] openai api key in config
\[\] testing core functions \[\] testing core functions
\[\] three js graph explorer \[\] three js graph explorer
\[\] three js vector explorer \[\] three js vector explorer
@@ -11,6 +8,7 @@
\[x\] chat functionality \[x\] chat functionality
\[x\] chat history \[x\] chat history
\[x\] chat styling overhaul \[x\] chat styling overhaul
\[x\] configs primarily get envs
\[x\] fix patch_text_content \[x\] fix patch_text_content
\[x\] gdpr \[x\] gdpr
\[x\] html ingression \[x\] html ingression
@@ -22,7 +20,9 @@
\[x\] link to ingressed urls or archives \[x\] link to ingressed urls or archives
\[x\] macro for pagedata? \[x\] macro for pagedata?
\[x\] make sure error messages render correctly \[x\] make sure error messages render correctly
\[x\] markdown rendering in client
\[x\] on updates of knowledgeentity create new embeddings \[x\] on updates of knowledgeentity create new embeddings
\[x\] openai api key in config
\[x\] option to set models, query and processing \[x\] option to set models, query and processing
\[x\] redirects \[x\] redirects
\[x\] restrict retrieval to users own objects \[x\] restrict retrieval to users own objects