mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-20 16:01:22 +02:00
release: 1.0.0
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
{% extends "modal_base.html" %}
|
||||
|
||||
{% block modal_class %}max-w-3xl{% endblock %}
|
||||
|
||||
{% block form_attributes %}
|
||||
hx-patch="/update-image-prompt"
|
||||
hx-target="#system_prompt_section"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends "modal_base.html" %}
|
||||
|
||||
{% block modal_class %}max-w-3xl{% endblock %}
|
||||
|
||||
{% block form_attributes %}
|
||||
hx-patch="/update-ingestion-prompt"
|
||||
hx-target="#system_prompt_section"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends "modal_base.html" %}
|
||||
|
||||
{% block modal_class %}max-w-3xl{% endblock %}
|
||||
|
||||
{% block form_attributes %}
|
||||
hx-patch="/update-query-prompt"
|
||||
hx-target="#system_prompt_section"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<body class="relative" hx-ext="head-support">
|
||||
<div class="drawer lg:drawer-open">
|
||||
<body class="relative">
|
||||
<div id="main-content-wrapper" class="drawer lg:drawer-open">
|
||||
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<!-- Page Content -->
|
||||
<div class="drawer-content flex flex-col h-screen">
|
||||
@@ -14,6 +14,7 @@
|
||||
{% block main %}{% endblock %}
|
||||
<div class="p32 min-h-[10px]"></div>
|
||||
</main>
|
||||
{% block overlay %}{% endblock %}
|
||||
</div>
|
||||
<!-- Sidebar -->
|
||||
{% if user %}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
{% block title %}Minne - Chat{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="/assets/htmx-ext-sse.js" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="flex grow relative justify-center mt-2 sm:mt-4">
|
||||
<div class="container">
|
||||
@@ -17,32 +13,69 @@
|
||||
</section>
|
||||
<div id="chat-scroll-container" class="overflow-auto hide-scrollbar">
|
||||
{% include "chat/history.html" %}
|
||||
{% include "chat/new_message_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function doScrollChatToBottom() {
|
||||
const mainScroll = document.querySelector('main');
|
||||
if (mainScroll) mainScroll.scrollTop = mainScroll.scrollHeight;
|
||||
|
||||
const chatScroll = document.getElementById('chat-scroll-container');
|
||||
if (chatScroll) chatScroll.scrollTop = chatScroll.scrollHeight;
|
||||
|
||||
const chatContainer = document.getElementById('chat_container');
|
||||
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
function scrollChatToBottom() {
|
||||
requestAnimationFrame(() => {
|
||||
const mainScroll = document.querySelector('main');
|
||||
if (mainScroll) mainScroll.scrollTop = mainScroll.scrollHeight;
|
||||
|
||||
const chatScroll = document.getElementById('chat-scroll-container');
|
||||
if (chatScroll) chatScroll.scrollTop = chatScroll.scrollHeight;
|
||||
|
||||
const chatContainer = document.getElementById('chat_container');
|
||||
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
});
|
||||
if (!window.location.pathname.startsWith('/chat')) return;
|
||||
requestAnimationFrame(doScrollChatToBottom);
|
||||
}
|
||||
|
||||
window.scrollChatToBottom = scrollChatToBottom;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', scrollChatToBottom);
|
||||
// Delay initial scroll to avoid interfering with view transition
|
||||
document.addEventListener('DOMContentLoaded', () => setTimeout(scrollChatToBottom, 350));
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', scrollChatToBottom);
|
||||
document.body.addEventListener('htmx:afterSettle', scrollChatToBottom);
|
||||
function handleChatSwap(e) {
|
||||
if (!window.location.pathname.startsWith('/chat')) return;
|
||||
// Full page swap: delay for view transition; partial swap: immediate
|
||||
if (e.detail && e.detail.target && e.detail.target.tagName === 'BODY') {
|
||||
setTimeout(scrollChatToBottom, 350);
|
||||
} else {
|
||||
scrollChatToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupChatListeners(e) {
|
||||
if (e.detail && e.detail.target && e.detail.target.tagName === 'BODY') {
|
||||
document.body.removeEventListener('htmx:afterSwap', window._chatEventHandlers.afterSwap);
|
||||
document.body.removeEventListener('htmx:afterSettle', window._chatEventHandlers.afterSettle);
|
||||
document.body.removeEventListener('htmx:beforeSwap', window._chatEventHandlers.beforeSwap);
|
||||
delete window._chatEventHandlers;
|
||||
window._chatListenersAttached = false;
|
||||
}
|
||||
}
|
||||
|
||||
window._chatEventHandlers = {
|
||||
afterSwap: handleChatSwap,
|
||||
afterSettle: handleChatSwap,
|
||||
beforeSwap: cleanupChatListeners
|
||||
};
|
||||
|
||||
if (!window._chatListenersAttached) {
|
||||
document.body.addEventListener('htmx:afterSwap', window._chatEventHandlers.afterSwap);
|
||||
document.body.addEventListener('htmx:afterSettle', window._chatEventHandlers.afterSettle);
|
||||
document.body.addEventListener('htmx:beforeSwap', window._chatEventHandlers.beforeSwap);
|
||||
window._chatListenersAttached = true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block overlay %}
|
||||
{% include "chat/new_message_form.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="fixed bottom-0 left-0 right-0 lg:left-72 z-20">
|
||||
<div class="mx-auto max-w-3xl px-4 pb-3">
|
||||
<div class="nb-panel p-2">
|
||||
<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">
|
||||
<textarea autofocus required name="content" placeholder="Type your message…" rows="3"
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
{% if text_contents|length > 0 %}
|
||||
<div class="nb-masonry w-full">
|
||||
{% for text_content in text_contents %}
|
||||
<article class="nb-card cursor-pointer mx-auto mb-4 w-full max-w-[92vw] space-y-3 sm:max-w-none"
|
||||
<article class="nb-card cursor-pointer mx-auto mb-4 w-full space-y-3"
|
||||
hx-get="/content/{{ text_content.id }}/read" hx-target="#modal" hx-swap="innerHTML">
|
||||
{% if text_content.url_info %}
|
||||
<figure class="-mx-4 -mt-4 border-b-2 border-neutral bg-base-200">
|
||||
<figure class="nb-evidence-frame -mx-4 -mt-4 mb-3">
|
||||
<img class="w-full h-auto" src="/file/{{text_content.url_info.image_id}}" alt="website screenshot" />
|
||||
</figure>
|
||||
{% endif %}
|
||||
{% if text_content.file_info.mime_type == "image/png" or text_content.file_info.mime_type == "image/jpeg" %}
|
||||
<figure class="-mx-4 -mt-4 border-b-2 border-neutral bg-base-200">
|
||||
<figure class="nb-evidence-frame -mx-4 -mt-4 mb-3">
|
||||
<img class="w-full h-auto" src="/file/{{text_content.file_info.id}}" alt="{{text_content.file_info.file_name}}" />
|
||||
</figure>
|
||||
{% endif %}
|
||||
@@ -31,10 +31,10 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<p class="text-xs opacity-60 shrink-0">
|
||||
<p class="nb-data text-xs opacity-60 shrink-0">
|
||||
{{ text_content.created_at | datetimeformat(format="short", tz=user.timezone) }}
|
||||
</p>
|
||||
<span class="nb-badge">{{ text_content.category }}</span>
|
||||
<span class="nb-badge nb-data">{{ text_content.category }}</span>
|
||||
<div class="flex gap-2" hx-on:click="event.stopPropagation()">
|
||||
{% if text_content.url_info %}
|
||||
<a href="{{text_content.url_info.url}}" target="_blank" rel="noopener noreferrer"
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
{% block title %}Minne - Dashboard{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="/assets/htmx-ext-sse.js" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4 w-full">
|
||||
<div class="container">
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/assets/htmx.min.js" defer></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/htmx-head-ext.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>
|
||||
|
||||
<!-- Global View Transition -->
|
||||
<meta name="view-transition" content="same-origin" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="/assets/icon/favicon.ico">
|
||||
@@ -38,6 +42,7 @@
|
||||
(function wait_for_htmx() {
|
||||
if (window.htmx) {
|
||||
htmx.config.globalViewTransitions = true;
|
||||
htmx.config.selfRequestsOnly = false;
|
||||
} else {
|
||||
setTimeout(wait_for_htmx, 50);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,78 @@
|
||||
{% extends "modal_base.html" %}
|
||||
|
||||
{% block modal_class %}max-w-3xl{% endblock %}
|
||||
|
||||
{% block form_attributes %}
|
||||
hx-post="/ingress-form"
|
||||
enctype="multipart/form-data"
|
||||
{% endblock %}
|
||||
|
||||
{% block modal_content %}
|
||||
<h3 class="text-xl font-extrabold tracking-tight">Add New Content</h3>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 class="text-xl font-extrabold tracking-tight pr-8">Add New Content</h3>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<!-- Content Source -->
|
||||
<label class="w-full">
|
||||
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Content</div>
|
||||
<textarea name="content" class="nb-input w-full min-h-28"
|
||||
<div class="nb-label mb-1">Content</div>
|
||||
<textarea name="content" class="nb-input w-full" rows="4" autofocus
|
||||
placeholder="Paste a URL or type/paste text to ingest…">{{ content }}</textarea>
|
||||
</label>
|
||||
|
||||
<label class="w-full">
|
||||
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Context</div>
|
||||
<textarea name="context" class="nb-input w-full min-h-24"
|
||||
placeholder="Optional: add context to guide how the content should be interpreted…">{{ context }}</textarea>
|
||||
<!-- Context (Optional) -->
|
||||
<label class="w-full mt-6">
|
||||
<div class="nb-label mb-1 flex justify-between items-center">
|
||||
<span>Context</span>
|
||||
<!-- Tufte-style annotation: clean, small caps, structural -->
|
||||
<span class="text-[10px] tracking-widest uppercase border border-neutral px-1.5 py-px bg-transparent opacity-60">Optional</span>
|
||||
</div>
|
||||
<textarea name="context" class="nb-input w-full" rows="2"
|
||||
placeholder="Guide how this content should be interpreted…">{{ context }}</textarea>
|
||||
</label>
|
||||
|
||||
<label class="w-full">
|
||||
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Category</div>
|
||||
<input type="text" name="category" class="nb-input validator w-full" value="{{ category }}" list="category-list" required />
|
||||
<datalist id="category-list">
|
||||
{% for category in user_categories %}
|
||||
<option value="{{ category }}" />
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
<div class="validator-hint hidden text-xs opacity-70 mt-1">Category is required</div>
|
||||
</label>
|
||||
<!-- Metadata Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-12 gap-y-8 items-start mt-6">
|
||||
|
||||
<!-- Category -->
|
||||
<label class="w-full">
|
||||
<div class="nb-label mb-1">Category <span class="text-error font-bold" title="Required">*</span></div>
|
||||
<div class="relative">
|
||||
<input type="text" name="category" class="nb-input validator w-full pr-8" value="{{ category }}" list="category-list" required placeholder="Select or type..." />
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none opacity-50">
|
||||
{% include "icons/chevron_icon.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<datalist id="category-list">
|
||||
{% for category in user_categories %}
|
||||
<option value="{{ category }}" />
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
<div class="validator-hint hidden text-xs opacity-70 mt-1 text-error">Category is required</div>
|
||||
</label>
|
||||
|
||||
<label class="w-full">
|
||||
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Files</div>
|
||||
<input type="file" name="files" multiple class="file-input w-full rounded-none border-2 border-neutral" />
|
||||
</label>
|
||||
<!-- Dimensional File Drop Zone -->
|
||||
<div class="w-full">
|
||||
<div class="nb-label mb-1">Files</div>
|
||||
<!-- "Card" style dropzone: solid border, hard shadow, lift on hover -->
|
||||
<div class="relative w-full h-32 group bg-base-100 border-2 border-neutral shadow-[4px_4px_0_0_#000] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[6px_6px_0_0_#000] transition-all duration-150">
|
||||
<!-- Visual Facade -->
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm font-medium text-neutral pointer-events-none">
|
||||
<div class="p-2 border-2 border-neutral rounded-none bg-base-200 group-hover:bg-base-100 transition-colors">
|
||||
<span class="w-6 h-6 block">{% include "icons/document_icon.html" %}</span>
|
||||
</div>
|
||||
<span id="file-label-text" class="text-center px-4 text-xs uppercase tracking-wide">Drop files or click</span>
|
||||
</div>
|
||||
<!-- Actual Input -->
|
||||
<input type="file" name="files" multiple
|
||||
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
onchange="const count = this.files.length; document.getElementById('file-label-text').innerText = count > 0 ? count + ' FILE' + (count !== 1 ? 'S' : '') + ' SELECTED' : 'DROP FILES OR CLICK';" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-message" class="text-error text-center {% if not error %}hidden{% endif %}">{{ error }}</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('modal_form');
|
||||
@@ -51,8 +88,9 @@ enctype="multipart/form-data"
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block primary_actions %}
|
||||
<button type="submit" class="nb-btn nb-cta">
|
||||
<button type="submit" class="nb-btn nb-cta w-full sm:w-auto">
|
||||
Add Content
|
||||
</button>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<dialog id="body_modal" class="modal">
|
||||
<div
|
||||
class="modal-box rounded-none border-2 border-neutral bg-base-100 shadow-[8px_8px_0_0_#000] {% block modal_class %}{% endblock %}">
|
||||
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 -->
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-square btn-ghost absolute right-2 top-2 z-10"
|
||||
onclick="document.getElementById('body_modal').close()"
|
||||
aria-label="Close modal">
|
||||
{% include "icons/x_icon.html" %}
|
||||
</button>
|
||||
|
||||
<form id="modal_form" {% block form_attributes %}{% endblock %}>
|
||||
<div class="flex flex-col flex-1 gap-4">
|
||||
<div class="flex flex-col flex-1 gap-5">
|
||||
{% block modal_content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="u-hairline mt-4 pt-3 flex flex-col gap-2 sm:flex-row sm:justify-end sm:items-center">
|
||||
<!-- Close button (always visible) -->
|
||||
<button type="button" class="nb-btn w-full sm:w-auto" onclick="document.getElementById('body_modal').close()">
|
||||
Close
|
||||
<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()">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<!-- Primary actions block -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav class="sticky top-0 z-10 nb-panel nb-panel-canvas border-t-0">
|
||||
<nav class="sticky top-0 z-10 nb-panel nb-panel-canvas border-t-0" style="view-transition-name: navbar; contain: layout;">
|
||||
<div class="container mx-auto navbar">
|
||||
<div class="mr-2 flex-1">
|
||||
{% include "searchbar.html" %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block modal_content %}
|
||||
<h3 class="text-xl font-extrabold tracking-tight">
|
||||
<div class="flex items-center gap-2" id="title-container">
|
||||
<div class="flex items-center gap-2 {% if is_editing_title %}hidden{% endif %}" id="title-container">
|
||||
<span class="font-semibold text-lg flex-1 truncate" id="title-display">{{ scratchpad.title }}</span>
|
||||
<button type="button" onclick="editTitle()" class="nb-btn nb-btn-sm btn-ghost">
|
||||
{% include "icons/edit_icon.html" %} Edit title
|
||||
@@ -15,9 +15,9 @@
|
||||
|
||||
<!-- Hidden title form -->
|
||||
<form id="title-form" hx-patch="/scratchpad/{{ scratchpad.id }}/title" hx-target="#body_modal" hx-swap="outerHTML"
|
||||
class="hidden flex items-center gap-2">
|
||||
class="{% if not is_editing_title %}hidden{% endif %} flex items-center gap-2">
|
||||
<input type="text" name="title" value="{{ scratchpad.title }}"
|
||||
class="nb-input nb-input-sm font-semibold text-lg flex-1" id="title-input">
|
||||
class="nb-input nb-input-sm font-semibold text-lg flex-1" id="title-input" {% if is_editing_title %}autofocus{% endif %}>
|
||||
<button type="submit" class="nb-btn nb-btn-sm">{% include "icons/check_icon.html" %}</button>
|
||||
<button type="button" onclick="cancelEditTitle()" class="nb-btn nb-btn-sm btn-ghost">{% include "icons/x_icon.html" %}</button>
|
||||
</form>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
<!-- Theme switch script -->
|
||||
<script>
|
||||
const initializeTheme = () => {
|
||||
console.log("Initializing theme toggle...");
|
||||
const themeToggle = document.querySelector('.theme-controller');
|
||||
if (!themeToggle) {
|
||||
console.log("Theme toggle not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Initialize theme from local storage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const initialTheme = savedTheme ? savedTheme : (prefersDark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
themeToggle.checked = initialTheme === 'dark';
|
||||
|
||||
// Update theme and local storage on toggle
|
||||
themeToggle.addEventListener('change', () => {
|
||||
const theme = themeToggle.checked ? 'dark' : 'light';
|
||||
console.log("Theme switched to:", theme);
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
console.log("Theme toggle initialized.");
|
||||
};
|
||||
|
||||
// Run the initialization after the DOM is fully loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log("DOM fully loaded. Initializing theme toggle...");
|
||||
initializeTheme();
|
||||
});
|
||||
|
||||
// Reinitialize theme toggle after HTMX swaps
|
||||
document.addEventListener('htmx:afterSwap', initializeTheme);
|
||||
document.addEventListener('htmx:afterSettle', initializeTheme);
|
||||
</script>
|
||||
@@ -14,7 +14,7 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<div class="drawer-side z-20">
|
||||
<div class="drawer-side z-20" style="view-transition-name: sidebar; contain: layout;">
|
||||
<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">
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
|
||||
<!-- === MIDDLE SCROLLABLE SECTION === -->
|
||||
<span class="px-4 py-2 font-semibold tracking-wide">Recent Chats</span>
|
||||
<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 %}
|
||||
|
||||
Reference in New Issue
Block a user