mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-24 09:48:32 +02:00
feat: markdown formatting complete
streaming and history implemented
This commit is contained in:
@@ -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}}');
|
});
|
||||||
|
// Buffer store for markdown, keyed by message id
|
||||||
|
window.markdownBuffer = window.markdownBuffer || {};
|
||||||
|
document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
|
||||||
|
const spinner = document.querySelector('.loading-id-{{user_message.id}}');
|
||||||
|
if (spinner) spinner.style.display = 'none';
|
||||||
|
|
||||||
// Hiding the loading spinner before data is swapped in
|
const el = document.getElementById('ai-message-content-{{user_message.id}}');
|
||||||
if (loadingSpinner) {
|
if (e.detail.elt !== el) return;
|
||||||
loadingSpinner.style.display = 'none';
|
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 || '');
|
||||||
// Add listener for after content is settled
|
// Render buffer (with newline fix) on *every* chunk
|
||||||
document.body.addEventListener('htmx:afterSettle', function (evt) {
|
el.innerHTML = marked.parse(
|
||||||
// Check if the settled element has our specific class
|
window.markdownBuffer[msgId].replace(/\\n/g, '\n')
|
||||||
// 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.
|
scrollChatToBottom();
|
||||||
const messageId = "{{user_message.id}}"; // Get the ID from the template context
|
});
|
||||||
const targetBubble = document.querySelector(`.ai-message-content-${messageId}`);
|
document.body.addEventListener('htmx:sseClose', function () {
|
||||||
|
const el = document.getElementById('ai-message-content-{{user_message.id}}');
|
||||||
// Ensure we have the marked library and the target exists
|
const msgId = '{{user_message.id}}';
|
||||||
if (targetBubble && typeof marked !== 'undefined') {
|
if (el && window.markdownBuffer[msgId]) {
|
||||||
// Get the raw text content (which includes previously streamed parts)
|
el.innerHTML = marked.parse(
|
||||||
// Exclude the spinner if it's still somehow there, though it should be hidden.
|
window.markdownBuffer[msgId].replace(/\\n/g, '\n')
|
||||||
let rawContent = '';
|
);
|
||||||
targetBubble.childNodes.forEach(node => {
|
delete window.markdownBuffer[msgId]; // Clean up in multichat
|
||||||
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);
|
|
||||||
// Sanitize BEFORE inserting potentially harmful HTML from Markdown
|
|
||||||
// It's better to sanitize *after* rendering if using DOMPurify
|
|
||||||
targetBubble.innerHTML = marked.parse(rawContent);
|
|
||||||
// Optional: Sanitize with DOMPurify *after* parsing for security
|
|
||||||
// if (typeof DOMPurify !== 'undefined') {
|
|
||||||
// targetBubble.innerHTML = DOMPurify.sanitize(marked.parse(rawContent));
|
|
||||||
// } else {
|
|
||||||
// targetBubble.innerHTML = marked.parse(rawContent); // Use with caution if markdown source isn't trusted
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
6
todo.md
6
todo.md
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user