mirror of
https://github.com/perstarkse/minne.git
synced 2026-07-04 03:51:43 +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:
@@ -19,17 +19,12 @@ hx-swap="outerHTML"
|
||||
{% endblock %}
|
||||
|
||||
{% block primary_actions %}
|
||||
<button type="button" class="nb-btn w-full sm:w-auto sm:mr-2" id="reset_prompt_button">
|
||||
<button type="button" class="nb-btn w-full sm:w-auto sm:mr-2"
|
||||
data-reset-target="textarea[name=image_processing_prompt]">
|
||||
Reset to Default
|
||||
</button>
|
||||
|
||||
<textarea id="default_prompt_content" style="display:none;">{{ default_image_prompt }}</textarea>
|
||||
<script>
|
||||
document.getElementById('reset_prompt_button').addEventListener('click', function () {
|
||||
const defaultContent = document.getElementById('default_prompt_content').value;
|
||||
document.querySelector('textarea[name=image_processing_prompt]').value = defaultContent;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button type="submit" class="nb-btn nb-cta w-full sm:w-auto">
|
||||
<span class="htmx-indicator hidden">
|
||||
|
||||
@@ -19,17 +19,12 @@ hx-swap="outerHTML"
|
||||
{% endblock %}
|
||||
|
||||
{% block primary_actions %}
|
||||
<button type="button" class="nb-btn w-full sm:w-auto sm:mr-2" id="reset_prompt_button">
|
||||
<button type="button" class="nb-btn w-full sm:w-auto sm:mr-2"
|
||||
data-reset-target="textarea[name=ingestion_system_prompt]">
|
||||
Reset to Default
|
||||
</button>
|
||||
|
||||
<textarea id="default_prompt_content" style="display:none;">{{ default_ingestion_prompt }}</textarea>
|
||||
<script>
|
||||
document.getElementById('reset_prompt_button').addEventListener('click', function () {
|
||||
const defaultContent = document.getElementById('default_prompt_content').value;
|
||||
document.querySelector('textarea[name=ingestion_system_prompt]').value = defaultContent;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button type="submit" class="nb-btn nb-cta w-full sm:w-auto">
|
||||
<span class="htmx-indicator hidden">
|
||||
|
||||
@@ -19,17 +19,12 @@ hx-swap="outerHTML"
|
||||
{% endblock %}
|
||||
|
||||
{% block primary_actions %}
|
||||
<button type="button" class="nb-btn w-full sm:w-auto sm:mr-2" id="reset_prompt_button">
|
||||
<button type="button" class="nb-btn w-full sm:w-auto sm:mr-2"
|
||||
data-reset-target="textarea[name=query_system_prompt]">
|
||||
Reset to Default
|
||||
</button>
|
||||
|
||||
<textarea id="default_prompt_content" style="display:none;">{{ default_query_prompt }}</textarea>
|
||||
<script>
|
||||
document.getElementById('reset_prompt_button').addEventListener('click', function () {
|
||||
const defaultContent = document.getElementById('default_prompt_content').value;
|
||||
document.querySelector('textarea[name=query_system_prompt]').value = defaultContent;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button type="submit" class="nb-btn nb-cta w-full sm:w-auto">
|
||||
<span class="htmx-indicator hidden">
|
||||
|
||||
@@ -23,5 +23,36 @@
|
||||
</div> <!-- End Drawer -->
|
||||
<div id="modal"></div>
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
|
||||
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (window.marked && !window.markedGlobalOptionsSet) {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
headerIds: false,
|
||||
mangle: false,
|
||||
smartLists: true,
|
||||
smartypants: true,
|
||||
xhtml: false
|
||||
});
|
||||
window.markedGlobalOptionsSet = true;
|
||||
}
|
||||
renderAllMarkdown();
|
||||
});
|
||||
document.body.addEventListener('htmx:afterSettle', renderAllMarkdown);
|
||||
|
||||
function renderAllMarkdown() {
|
||||
if (!window.marked) return;
|
||||
document.querySelectorAll('.markdown-content[data-content]').forEach(el => {
|
||||
const raw = el.getAttribute('data-content') || '';
|
||||
if (el.dataset.renderedContent !== raw) {
|
||||
el.innerHTML = marked.parse(raw);
|
||||
el.dataset.renderedContent = raw;
|
||||
}
|
||||
});
|
||||
}
|
||||
window.renderAllMarkdown = renderAllMarkdown;
|
||||
</script>
|
||||
</body>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,60 +1,62 @@
|
||||
<div class="drawer-side z-20">
|
||||
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
|
||||
<ul class="menu p-0 w-72 h-full nb-canvas text-base-content flex flex-col border-r-2 border-neutral">
|
||||
<nav class="flex flex-col h-full w-72 p-0 nb-canvas text-base-content border-r-2 border-neutral">
|
||||
<!-- === TOP FIXED SECTION === -->
|
||||
<div class="px-2 mt-4 space-y-3">
|
||||
<ul class="menu w-full px-2 mt-4 space-y-3">
|
||||
{% block sidebar_nav_items %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<!-- === MIDDLE SCROLLABLE SECTION === -->
|
||||
<span class="px-4 py-2 nb-label">Recent Chats</span>
|
||||
<div class="flex-1 overflow-y-auto space-y-1 custom-scrollbar">
|
||||
{% if conversation_archive is defined and conversation_archive %}
|
||||
{% for conversation in conversation_archive %}
|
||||
<li id="conversation-{{ conversation.id }}">
|
||||
{% if edit_conversation_id == conversation.id %}
|
||||
<!-- Edit mode -->
|
||||
<form hx-patch="/chat/{{ conversation.id }}/title" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="flex items-center gap-1 px-2 py-2 max-w-72 relative">
|
||||
<input type="text" name="title" value="{{ conversation.title }}" class="nb-input nb-input-sm max-w-52" />
|
||||
<div class="flex gap-0.5 absolute right-2">
|
||||
<button type="submit" class="btn btn-ghost btn-xs !p-0">{% include "icons/check_icon.html" %}</button>
|
||||
<button type="button" hx-get="/chat/sidebar" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="btn btn-ghost btn-xs !p-0">
|
||||
{% include "icons/x_icon.html" %}
|
||||
</button>
|
||||
<div class="flex-1 overflow-y-auto min-h-0 custom-scrollbar">
|
||||
<ul class="menu w-full space-y-1">
|
||||
{% if conversation_archive is defined and conversation_archive %}
|
||||
{% for conversation in conversation_archive %}
|
||||
<li id="conversation-{{ conversation.id }}">
|
||||
{% if edit_conversation_id == conversation.id %}
|
||||
<!-- Edit mode -->
|
||||
<form hx-patch="/chat/{{ conversation.id }}/title" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="flex items-center gap-1 px-2 py-2 max-w-72 relative">
|
||||
<input type="text" name="title" value="{{ conversation.title }}" class="nb-input nb-input-sm max-w-52" />
|
||||
<div class="flex gap-0.5 absolute right-2">
|
||||
<button type="submit" class="btn btn-ghost btn-xs !p-0">{% include "icons/check_icon.html" %}</button>
|
||||
<button type="button" hx-get="/chat/sidebar" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="btn btn-ghost btn-xs !p-0">
|
||||
{% include "icons/x_icon.html" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<!-- View mode -->
|
||||
<div class="flex w-full pl-4 pr-2 py-2">
|
||||
<a hx-boost="true" href="/chat/{{ conversation.id }}" class="flex-grow text-sm truncate">
|
||||
<span>{{ conversation.title }}</span>
|
||||
</a>
|
||||
<div class="flex items-center gap-0.5 ml-2">
|
||||
<button hx-get="/chat/{{ conversation.id }}/title" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="btn btn-ghost btn-xs">
|
||||
{% include "icons/edit_icon.html" %}
|
||||
</button>
|
||||
<button hx-delete="/chat/{{ conversation.id }}" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to delete this chat?" class="btn btn-ghost btn-xs">
|
||||
{% include "icons/delete_icon.html" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<!-- View mode -->
|
||||
<div class="flex w-full pl-4 pr-2 py-2">
|
||||
<a hx-boost="true" href="/chat/{{ conversation.id }}" class="flex-grow text-sm truncate">
|
||||
<span>{{ conversation.title }}</span>
|
||||
</a>
|
||||
<div class="flex items-center gap-0.5 ml-2">
|
||||
<button hx-get="/chat/{{ conversation.id }}/title" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
class="btn btn-ghost btn-xs">
|
||||
{% include "icons/edit_icon.html" %}
|
||||
</button>
|
||||
<button hx-delete="/chat/{{ conversation.id }}" hx-target=".drawer-side" hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to delete this chat?" class="btn btn-ghost btn-xs">
|
||||
{% include "icons/delete_icon.html" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- === BOTTOM FIXED SECTION === -->
|
||||
<div class="px-2 pb-4 space-y-3">
|
||||
<ul class="menu w-full px-2 pb-4 space-y-3">
|
||||
{% block sidebar_bottom_actions %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</ul>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -16,12 +16,20 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/assets/htmx.min.js" defer></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
htmx.config.globalViewTransitions = true;
|
||||
htmx.config.selfRequestsOnly = false;
|
||||
});
|
||||
</script>
|
||||
<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="/assets/marked.min.js" defer></script>
|
||||
<script src="/assets/knowledge-graph.js" defer></script>
|
||||
<script src="/assets/design-polish.js" defer></script>
|
||||
<script src="/assets/modal.js" defer></script>
|
||||
<script src="/assets/admin-prompt-reset.js" defer></script>
|
||||
|
||||
<!-- Global View Transition -->
|
||||
<meta name="view-transition" content="same-origin" />
|
||||
@@ -38,49 +46,7 @@
|
||||
{% block head %}{% endblock %}
|
||||
|
||||
</head>
|
||||
<script>
|
||||
(function wait_for_htmx() {
|
||||
if (window.htmx) {
|
||||
htmx.config.globalViewTransitions = true;
|
||||
htmx.config.selfRequestsOnly = false;
|
||||
} else {
|
||||
setTimeout(wait_for_htmx, 50);
|
||||
}
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<script defer>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (window.marked && !window.markedGlobalOptionsSet) {
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
headerIds: false,
|
||||
mangle: false,
|
||||
smartLists: true,
|
||||
smartypants: true,
|
||||
xhtml: false
|
||||
});
|
||||
window.markedGlobalOptionsSet = true;
|
||||
}
|
||||
renderAllMarkdown();
|
||||
});
|
||||
document.body.addEventListener('htmx:afterSettle', renderAllMarkdown);
|
||||
|
||||
function renderAllMarkdown() {
|
||||
if (!window.marked) return;
|
||||
document.querySelectorAll('.markdown-content[data-content]').forEach(el => {
|
||||
const raw = el.getAttribute('data-content') || '';
|
||||
if (el.dataset.renderedContent !== raw) {
|
||||
el.innerHTML = marked.parse(raw);
|
||||
el.dataset.renderedContent = raw;
|
||||
}
|
||||
});
|
||||
}
|
||||
window.renderAllMarkdown = renderAllMarkdown;
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -62,7 +62,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<input id="relationship_type_input" name="relationship_type" type="text" placeholder="RelatedTo"
|
||||
class="nb-input w-full new_relationship_input" value="{{ default_relationship_type }}" />
|
||||
class="nb-input w-full new_relationship_input" value="{{ default_relationship_type }}"
|
||||
hx-on:keydown="if(event.key==='Enter'){event.preventDefault();document.getElementById('save_relationship_button').click()}" />
|
||||
</td>
|
||||
<td>
|
||||
<button id="save_relationship_button" type="button" class="nb-btn btn-sm" hx-post="/knowledge-relationship"
|
||||
@@ -74,11 +75,3 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('relationship_type_input').addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault(); // Prevent form submission if within a form
|
||||
document.getElementById('save_relationship_button').click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,46 +2,32 @@
|
||||
<div
|
||||
class="modal-box relative rounded-none border-2 border-neutral bg-base-100 shadow-[8px_8px_0_0_#000] p-6 {% block modal_class %}max-w-lg{% endblock %}">
|
||||
|
||||
<!-- God Level UX: Explicit Escape Hatch -->
|
||||
<!-- Close control (always visible; does not depend on form submit/cancel) -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-square btn-ghost absolute right-2 top-2 z-10"
|
||||
onclick="document.getElementById('body_modal').close()"
|
||||
hx-on:click="document.getElementById('body_modal').close()"
|
||||
aria-label="Close modal">
|
||||
{% include "icons/x_icon.html" %}
|
||||
</button>
|
||||
|
||||
<form id="modal_form" {% block form_attributes %}{% endblock %}>
|
||||
{# Default: one outer #modal_form. Modals with multiple forms (scratchpad editor)
|
||||
override modal_form_open / modal_form_close — nested <form> is invalid HTML. #}
|
||||
{% block modal_form_open %}<form id="modal_form" hx-on::after-request="if(event.detail.successful) document.getElementById('body_modal').close()" {% block form_attributes %}{% endblock %}>{% endblock %}
|
||||
<div class="flex flex-col flex-1 gap-5">
|
||||
{% block modal_content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-2 flex flex-col gap-2 sm:flex-row sm:justify-end sm:items-center">
|
||||
<!-- Secondary Action: Ghost style to reduce noise -->
|
||||
<button type="button" class="btn btn-ghost rounded-none w-full sm:w-auto hover:bg-neutral/10" onclick="document.getElementById('body_modal').close()">
|
||||
<!-- Dismiss without submitting -->
|
||||
<button type="button" class="btn btn-ghost rounded-none w-full sm:w-auto hover:bg-neutral/10" hx-on:click="document.getElementById('body_modal').close()">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<!-- Primary actions block -->
|
||||
{% block primary_actions %}{% endblock %}
|
||||
</div>
|
||||
</form>
|
||||
{% block modal_form_close %}</form>{% endblock %}
|
||||
</div>
|
||||
<script>
|
||||
// Auto-open modal when injected
|
||||
document.getElementById('body_modal').showModal();
|
||||
|
||||
// Close modal on successful form submission
|
||||
document.getElementById('modal_form')
|
||||
.addEventListener('htmx:afterRequest', (evt) => {
|
||||
if (evt.detail.elt !== evt.currentTarget) return; // ignore inner htmx requests
|
||||
if (evt.detail.successful) document.getElementById('body_modal').close();
|
||||
});
|
||||
|
||||
// Clear modal content on close to prevent browser back from reopening it
|
||||
document.getElementById('body_modal').addEventListener('close', (evt) => {
|
||||
evt.target.innerHTML = '';
|
||||
});
|
||||
</script>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
{% block modal_class %}w-11/12 max-w-[90ch] max-h-[95%] overflow-y-auto{% endblock %}
|
||||
|
||||
{% block form_attributes %}{% endblock %}
|
||||
{# title-form, auto-save-form, ingest-form, archive-form — override modal_base
|
||||
wrapper blocks so these are not nested inside #modal_form. #}
|
||||
{% block modal_form_open %}<div class="contents">{% endblock %}
|
||||
{% block modal_form_close %}</div>{% endblock %}
|
||||
|
||||
{% block modal_content %}
|
||||
<h3 class="text-xl font-extrabold tracking-tight">
|
||||
@@ -34,7 +37,7 @@
|
||||
|
||||
<form id="auto-save-form"
|
||||
hx-patch="/scratchpad/{{ scratchpad.id }}/auto-save"
|
||||
hx-trigger="keyup changed delay:2s, focusout"
|
||||
hx-trigger="keyup changed delay:2s, focusout delay:150ms"
|
||||
hx-indicator="#save-indicator"
|
||||
hx-swap="none"
|
||||
class="flex flex-col gap-2">
|
||||
@@ -64,8 +67,8 @@
|
||||
hx-on::after-request="if(event.detail.successful) document.getElementById('body_modal').close()"
|
||||
class="inline flex flex-col gap-3"
|
||||
id="ingest-form">
|
||||
<button type="button" class="nb-btn nb-cta" onclick="toggleIngestConfirmation(true)"
|
||||
data-role="ingest-trigger">
|
||||
<button type="button" class="nb-btn nb-cta" data-role="ingest-trigger"
|
||||
hx-on:mousedown="event.preventDefault(); toggleIngestConfirmation(true)">
|
||||
{% include "icons/send_icon.html" %} Ingest as Content
|
||||
</button>
|
||||
<div id="ingest-warning"
|
||||
@@ -81,7 +84,8 @@
|
||||
<button type="submit" class="nb-btn nb-btn-sm nb-cta">
|
||||
Confirm ingest
|
||||
</button>
|
||||
<button type="button" class="nb-btn nb-btn-sm btn-ghost" onclick="toggleIngestConfirmation(false)">
|
||||
<button type="button" class="nb-btn nb-btn-sm btn-ghost"
|
||||
hx-on:click="toggleIngestConfirmation(false)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
hx-target="#modal" hx-swap="innerHTML">{% include "icons/send_icon.html" %} Add
|
||||
Content</button>
|
||||
</li>
|
||||
<div class="u-hairline mt-4"></div>
|
||||
<li role="separator" class="u-hairline mt-4 list-none"></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar_bottom_actions %}
|
||||
|
||||
Reference in New Issue
Block a user