mirror of
https://github.com/perstarkse/minne.git
synced 2026-07-05 20:41:41 +02:00
fix: html-router modals and add insta snapshot tests.
Avoid nested forms in the scratchpad editor, centralize modal lifecycle in modal.js, return HTMX partials from archive, and add template compile plus layout snapshots.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
{% include "chat/streaming_response.html" %}
|
||||
<!-- OOB swap targeting the form element directly -->
|
||||
<form id="chat-form" hx-post="/chat/{{conversation.id}}" hx-target="#chat_container" hx-swap="beforeend"
|
||||
class="relative flex gap-2" hx-swap-oob="true">
|
||||
class="relative flex gap-2" hx-swap-oob="true"
|
||||
hx-on::after-request="if(event.detail.successful) document.getElementById('chat-input').value=''">
|
||||
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
|
||||
class="nb-input h-24 pr-8 pl-2 pt-2 pb-2 flex-grow resize-none" id="chat-input"></textarea>
|
||||
class="nb-input h-24 pr-8 pl-2 pt-2 pb-2 flex-grow resize-none" id="chat-input"
|
||||
hx-on:keydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();htmx.trigger('#chat-form','submit')}"></textarea>
|
||||
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-1">
|
||||
{% include "icons/send_icon.html" %}
|
||||
</button>
|
||||
@@ -13,24 +15,8 @@
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
const newChatStreamId = 'ai-stream-{{ user_message.id }}';
|
||||
|
||||
document.getElementById('chat-input').addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
htmx.trigger('#chat-form', 'submit');
|
||||
}
|
||||
});
|
||||
// Clear textarea after successful submission
|
||||
document.getElementById('chat-form').addEventListener('htmx:afterRequest', function (e) {
|
||||
if (e.detail.successful) { // Check if the request was successful
|
||||
document.getElementById('chat-input').value = ''; // Clear the textarea
|
||||
}
|
||||
});
|
||||
|
||||
const refreshSidebarAfterFirstResponse = function (e) {
|
||||
const streamEl = document.getElementById(newChatStreamId);
|
||||
if (!streamEl || e.target !== streamEl) return;
|
||||
if (!e.target.closest('[data-message-id]')) return;
|
||||
|
||||
htmx.ajax('GET', '/chat/sidebar', {
|
||||
target: '.drawer-side',
|
||||
|
||||
@@ -2,26 +2,13 @@
|
||||
<div class="mx-auto max-w-3xl px-4 pb-3">
|
||||
<div class="nb-panel p-2 no-animation">
|
||||
<form hx-post="{% if conversation %} /chat/{{conversation.id}} {% else %} /chat {% endif %}"
|
||||
hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2 items-end" id="chat-form">
|
||||
hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2 items-end" id="chat-form"
|
||||
hx-on::after-request="if(event.detail.successful) document.getElementById('chat-input').value=''">
|
||||
<textarea autofocus required name="content" placeholder="Type your message…" rows="3"
|
||||
class="nb-input flex-grow min-h-24 pr-10 pl-2 pt-2 pb-2 resize-none" id="chat-input"></textarea>
|
||||
class="nb-input flex-grow min-h-24 pr-10 pl-2 pt-2 pb-2 resize-none" id="chat-input"
|
||||
hx-on:keydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();htmx.trigger('#chat-form','submit')}"></textarea>
|
||||
<button type="submit" class="nb-btn nb-cta h-10 px-3">{% include "icons/send_icon.html" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('chat-input').addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
htmx.trigger('#chat-form', 'submit');
|
||||
}
|
||||
});
|
||||
// Clear textarea after successful submission
|
||||
document.getElementById('chat-form').addEventListener('htmx:afterRequest', function (e) {
|
||||
if (e.detail.successful) { // Check if the request was successful
|
||||
document.getElementById('chat-input').value = ''; // Clear the textarea
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<div class="relative my-2">
|
||||
<button id="references-toggle-{{message.id}}"
|
||||
class="nb-btn btn-xs bg-base-100 hover:bg-base-200 flex items-center"
|
||||
onclick="toggleReferences('{{message.id}}')">
|
||||
<button id="references-toggle-{{message.id}}" data-message-id="{{message.id}}"
|
||||
class="references-toggle nb-btn btn-xs bg-base-100 hover:bg-base-200 flex items-center">
|
||||
REFERENCES
|
||||
{% include "icons/chevron_icon.html" %}
|
||||
</button>
|
||||
@@ -20,6 +19,19 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register global handlers once; this fragment is re-injected per SSE response.
|
||||
if (!window.__referencesInit) {
|
||||
window.__referencesInit = true;
|
||||
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.references-toggle');
|
||||
if (btn) toggleReferences(btn.dataset.messageId);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeReferenceTooltips);
|
||||
document.body.addEventListener('htmx:afterSwap', initializeReferenceTooltips);
|
||||
}
|
||||
|
||||
function toggleReferences(messageId) {
|
||||
const refsContent = document.getElementById('references-content-' + messageId);
|
||||
const refsList = document.getElementById('references-list-' + messageId);
|
||||
@@ -56,15 +68,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize portal tooltips
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initializeReferenceTooltips();
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function () {
|
||||
initializeReferenceTooltips();
|
||||
});
|
||||
|
||||
function initializeReferenceTooltips() {
|
||||
document.querySelectorAll('.reference-badge-container').forEach(container => {
|
||||
if (container.dataset.initialized === 'true') return;
|
||||
@@ -145,4 +148,7 @@
|
||||
container.dataset.initialized = 'true';
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize any badges present in this freshly injected fragment (idempotent).
|
||||
initializeReferenceTooltips();
|
||||
</script>
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat chat-start">
|
||||
<div id="ai-stream-{{user_message.id}}" hx-ext="sse"
|
||||
<div id="ai-stream-{{user_message.id}}" data-message-id="{{user_message.id}}" hx-ext="sse"
|
||||
sse-connect="/chat/response-stream?message_id={{user_message.id}}" sse-close="close_stream"
|
||||
hx-swap="beforeend">
|
||||
<div class="chat-bubble">
|
||||
<span class="loading loading-dots loading-sm loading-id-{{user_message.id}}"></span>
|
||||
<span class="loading loading-dots loading-sm" data-stream-spinner></span>
|
||||
<div class="markdown-content" id="ai-message-content-{{user_message.id}}" sse-swap="chat_message"></div>
|
||||
</div>
|
||||
<div sse-swap="references"></div>
|
||||
@@ -16,34 +16,39 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.markdownBuffer = window.markdownBuffer || {};
|
||||
document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
|
||||
const msgId = '{{ user_message.id }}';
|
||||
const spinner = document.querySelector('.loading-id-' + msgId);
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
const el = document.getElementById('ai-message-content-' + msgId);
|
||||
if (e.detail.elt !== el) return;
|
||||
e.preventDefault();
|
||||
window.markdownBuffer[msgId] = (window.markdownBuffer[msgId] || '') + (e.detail.data || '');
|
||||
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
|
||||
if (typeof window.scrollChatToBottom === "function") window.scrollChatToBottom();
|
||||
});
|
||||
document.body.addEventListener('htmx:sseClose', function (e) {
|
||||
const msgId = '{{ user_message.id }}';
|
||||
const streamEl = document.getElementById('ai-stream-' + msgId);
|
||||
if (streamEl && e.target !== streamEl) return;
|
||||
// Single delegated listener set; message identity comes from data-message-id.
|
||||
(function () {
|
||||
if (window.__streamHandlersInit) return;
|
||||
window.__streamHandlersInit = true;
|
||||
window.markdownBuffer = window.markdownBuffer || {};
|
||||
|
||||
const el = document.getElementById('ai-message-content-' + msgId);
|
||||
if (el && window.markdownBuffer[msgId]) {
|
||||
document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
|
||||
const root = e.detail.elt.closest('[data-message-id]');
|
||||
if (!root) return;
|
||||
const msgId = root.dataset.messageId;
|
||||
const spinner = root.querySelector('[data-stream-spinner]');
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
const el = document.getElementById('ai-message-content-' + msgId);
|
||||
if (e.detail.elt !== el) return;
|
||||
e.preventDefault();
|
||||
window.markdownBuffer[msgId] = (window.markdownBuffer[msgId] || '') + (e.detail.data || '');
|
||||
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
|
||||
delete window.markdownBuffer[msgId];
|
||||
if (typeof window.scrollChatToBottom === "function") window.scrollChatToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
if (streamEl) {
|
||||
document.body.addEventListener('htmx:sseClose', function (e) {
|
||||
const streamEl = e.target.closest('[data-message-id]');
|
||||
if (!streamEl) return;
|
||||
const msgId = streamEl.dataset.messageId;
|
||||
const el = document.getElementById('ai-message-content-' + msgId);
|
||||
if (el && window.markdownBuffer[msgId]) {
|
||||
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
|
||||
delete window.markdownBuffer[msgId];
|
||||
if (typeof window.scrollChatToBottom === "function") window.scrollChatToBottom();
|
||||
}
|
||||
streamEl.removeAttribute('sse-connect');
|
||||
streamEl.removeAttribute('sse-close');
|
||||
streamEl.removeAttribute('hx-ext');
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user