refactor: extendable templates

refactor: simplification

refactor: simplification
This commit is contained in:
Per Stark
2026-01-13 15:57:12 +01:00
parent 037057d108
commit 354dc727c1
32 changed files with 711 additions and 579 deletions

View File

@@ -29,15 +29,17 @@ impl HtmlState {
config: AppConfig, config: AppConfig,
reranker_pool: Option<Arc<RerankerPool>>, reranker_pool: Option<Arc<RerankerPool>>,
embedding_provider: Arc<EmbeddingProvider>, embedding_provider: Arc<EmbeddingProvider>,
template_engine: Option<Arc<TemplateEngine>>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let template_engine = create_template_engine!("templates"); let templates =
debug!("Template engine created for html_router."); template_engine.unwrap_or_else(|| Arc::new(create_template_engine!("templates")));
debug!("Template engine configured for html_router.");
Ok(Self { Ok(Self {
db, db,
openai_client, openai_client,
session_store, session_store,
templates: Arc::new(template_engine), templates,
config, config,
storage, storage,
reranker_pool, reranker_pool,

View File

@@ -0,0 +1,20 @@
{% extends 'admin/_layout.html' %}
{% block admin_navigation %}
<a href="/admin?section=overview"
class="nb-btn btn-sm px-4 {% if current_section == 'overview' %}nb-cta{% else %}btn-ghost{% endif %}">
Overview
</a>
<a href="/admin?section=models"
class="nb-btn btn-sm px-4 {% if current_section == 'models' %}nb-cta{% else %}btn-ghost{% endif %}">
Models
</a>
{% endblock %}
{% block admin_content %}
{% if current_section == 'models' %}
{% include 'admin/sections/models.html' %}
{% else %}
{% include 'admin/sections/overview.html' %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends 'body_base.html' %}
{% block title %}Minne - Admin{% endblock %}
{% block main %}
<div id="admin-shell" class="flex justify-center grow mt-2 sm:mt-4 pb-4">
<div class="container flex flex-col gap-4">
<section class="nb-panel p-4 sm:p-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 class="text-xl font-extrabold tracking-tight">Admin Controls</h1>
</div>
<div class="text-xs opacity-60 sm:text-right">
Signed in as <span class="font-medium">{{ user.email }}</span>
</div>
</section>
<nav class="nb-panel p-2 flex flex-wrap gap-2 text-sm" hx-boost="true" hx-target="#admin-shell"
hx-select="#admin-shell" hx-swap="outerHTML" hx-push-url="true">
{% block admin_navigation %}
{% endblock %}
</nav>
<div id="admin-content" class="flex flex-col gap-4">
{% block admin_content %}
{% endblock %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,38 +1 @@
{% extends 'body_base.html' %} {% extends "admin/_base.html" %}
{% block title %}Minne - Admin{% endblock %}
{% block main %}
<div id="admin-shell" class="flex justify-center grow mt-2 sm:mt-4 pb-4">
<div class="container flex flex-col gap-4">
<section class="nb-panel p-4 sm:p-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 class="text-xl font-extrabold tracking-tight">Admin Controls</h1>
</div>
<div class="text-xs opacity-60 sm:text-right">
Signed in as <span class="font-medium">{{ user.email }}</span>
</div>
</section>
<nav class="nb-panel p-2 flex flex-wrap gap-2 text-sm" hx-boost="true" hx-target="#admin-shell"
hx-select="#admin-shell" hx-swap="outerHTML" hx-push-url="true">
<a href="/admin?section=overview"
class="nb-btn btn-sm px-4 {% if current_section == 'overview' %}nb-cta{% else %}btn-ghost{% endif %}">
Overview
</a>
<a href="/admin?section=models"
class="nb-btn btn-sm px-4 {% if current_section == 'models' %}nb-cta{% else %}btn-ghost{% endif %}">
Models
</a>
</nav>
<div id="admin-content" class="flex flex-col gap-4">
{% if current_section == 'models' %}
{% include 'admin/sections/models.html' %}
{% else %}
{% include 'admin/sections/overview.html' %}
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends "auth/_settings_layout.html" %}
{% block settings_header %}
<h1 class="text-xl font-extrabold tracking-tight">Account Settings</h1>
{% endblock %}
{% block settings_left_column %}
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Email</div>
<input type="email" name="email" value="{{ user.email }}" class="nb-input w-full" disabled />
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">API Key</div>
{% block api_key_section %}
{% if user.api_key %}
<div class="relative">
<input id="api_key_input" type="text" name="api_key" value="{{ user.api_key }}"
class="nb-input w-full pr-14" disabled />
<button type="button" id="copy_api_key_btn" onclick="copy_api_key()"
class="absolute inset-y-0 right-0 flex items-center px-2 nb-btn btn-sm" aria-label="Copy API key"
title="Copy API key">
{% include "icons/clipboard_icon.html" %}
</button>
</div>
<a href="https://www.icloud.com/shortcuts/66985f7b98a74aaeac6ba29c3f1f0960"
class="nb-btn nb-cta mt-2 w-full">Download iOS shortcut</a>
{% else %}
<button hx-post="/set-api-key" class="nb-btn nb-cta w-full" hx-swap="outerHTML">Create API-Key</button>
{% endif %}
{% endblock %}
</label>
<script>
function copy_api_key() {
const input = document.getElementById('api_key_input');
if (!input) return;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(input.value)
.then(() => show_toast('API key copied!', 'success'))
.catch(() => show_toast('Copy failed', 'error'));
} else {
show_toast('Copy not supported', 'info');
}
}
</script>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Timezone</div>
{% block timezone_section %}
<select name="timezone" class="nb-select w-full" hx-patch="/update-timezone" hx-swap="outerHTML">
{% for tz in timezones %}
<option value="{{ tz }}" {% if tz==user.timezone %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
{% endblock %}
</label>
{% endblock %}
{% block settings_right_column %}
<div>
{% block change_password_section %}
<button hx-get="/change-password" hx-swap="outerHTML" class="nb-btn w-full">Change Password</button>
{% endblock %}
</div>
<div>
<button hx-delete="/delete-account"
hx-confirm="This action will permanently delete your account and all data associated. Are you sure you want to continue?"
class="nb-btn btn-error w-full">Delete Account</button>
</div>
{% endblock %}

View File

@@ -0,0 +1,10 @@
{% extends "head_base.html" %}
{% block title %}Minne - Auth{% endblock %}
{% block body %}
<div class="min-h-[100dvh] flex flex-col items-center justify-center">
{% block auth_content %}
{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends "body_base.html" %}
{% block title %}Minne - Account{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4">
<div class="container">
<section class="mb-4">
<div class="nb-panel p-3 flex items-center justify-between">
{% block settings_header %}
{% endblock %}
</div>
</section>
<section class="grid grid-cols-1 lg:grid-cols-2 gap-4 space-y-2">
<!-- Left column -->
<div class="nb-panel p-4 space-y-2 flex flex-col">
{% block settings_left_column %}
{% endblock %}
</div>
<!-- Right column -->
<div class="nb-panel p-4 space-y-2">
{% block settings_right_column %}
{% endblock %}
</div>
</section>
<div id="account-result" class="mt-4"></div>
</div>
</div>
{% endblock %}

View File

@@ -1,88 +1 @@
{% extends "body_base.html" %} {% extends "auth/_account_settings_core.html" %}
{% block title %}Minne - Account{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4">
<div class="container">
<section class="mb-4">
<div class="nb-panel p-3 flex items-center justify-between">
<h1 class="text-xl font-extrabold tracking-tight">Account Settings</h1>
</div>
</section>
<section class="grid grid-cols-1 lg:grid-cols-2 gap-4 space-y-2">
<!-- Left column -->
<div class="nb-panel p-4 space-y-2 flex flex-col">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Email</div>
<input type="email" name="email" value="{{ user.email }}" class="nb-input w-full" disabled />
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">API Key</div>
{% block api_key_section %}
{% if user.api_key %}
<div class="relative">
<input id="api_key_input" type="text" name="api_key" value="{{ user.api_key }}"
class="nb-input w-full pr-14" disabled />
<button type="button" id="copy_api_key_btn" onclick="copy_api_key()"
class="absolute inset-y-0 right-0 flex items-center px-2 nb-btn btn-sm" aria-label="Copy API key"
title="Copy API key">
{% include "icons/clipboard_icon.html" %}
</button>
</div>
<a href="https://www.icloud.com/shortcuts/66985f7b98a74aaeac6ba29c3f1f0960"
class="nb-btn nb-cta mt-2 w-full">Download iOS shortcut</a>
{% else %}
<button hx-post="/set-api-key" class="nb-btn nb-cta w-full" hx-swap="outerHTML">Create API-Key</button>
{% endif %}
{% endblock %}
</label>
<script>
function copy_api_key() {
const input = document.getElementById('api_key_input');
if (!input) return;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(input.value)
.then(() => show_toast('API key copied!', 'success'))
.catch(() => show_toast('Copy failed', 'error'));
} else {
show_toast('Copy not supported', 'info');
}
}
</script>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Timezone</div>
{% block timezone_section %}
<select name="timezone" class="nb-select w-full" hx-patch="/update-timezone" hx-swap="outerHTML">
{% for tz in timezones %}
<option value="{{ tz }}" {% if tz==user.timezone %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
{% endblock %}
</label>
</div>
<!-- Right column -->
<div class="nb-panel p-4 space-y-2">
<div>
{% block change_password_section %}
<button hx-get="/change-password" hx-swap="outerHTML" class="nb-btn w-full">Change Password</button>
{% endblock %}
</div>
<div>
<button hx-delete="/delete-account"
hx-confirm="This action will permanently delete your account and all data associated. Are you sure you want to continue?"
class="nb-btn btn-error w-full">Delete Account</button>
</div>
</div>
</section>
<div id="account-result" class="mt-4"></div>
</div>
</div>
{% endblock %}

View File

@@ -1,9 +1,7 @@
{% extends "head_base.html" %} {% extends "auth/_layout.html" %}
{% block title %}Minne - Sign in{% endblock %} {% block title %}Minne - Sign in{% endblock %}
{% block body %} {% block auth_content %}
<div class="min-h-[100dvh] flex">
{% include "auth/signin_form.html" %} {% include "auth/signin_form.html" %}
</div> {% endblock %}
{% endblock %}

View File

@@ -1,9 +1,8 @@
{% extends "head_base.html" %} {% extends "auth/_layout.html" %}
{% block title %}Minne - Sign up{% endblock %} {% block title %}Minne - Sign up{% endblock %}
{% block body %} {% block auth_content %}
<div class="min-h-[100dvh] flex items-center">
<div class="container mx-auto px-4 sm:max-w-md"> <div class="container mx-auto px-4 sm:max-w-md">
<div class="nb-card p-5"> <div class="nb-card p-5">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
@@ -46,10 +45,9 @@
</div> </div>
</div> </div>
</div> </div>
</div> <script>
<script> // Detect timezone and set hidden input
// Detect timezone and set hidden input const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; document.getElementById("timezone").value = timezone;
document.getElementById("timezone").value = timezone; </script>
</script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,78 @@
{% extends 'body_base.html' %}
{% block title %}Minne - Chat{% endblock %}
{% block main %}
<div class="flex grow relative justify-center mt-2 sm:mt-4">
<div class="container">
<section class="mb-3">
<div class="nb-panel p-3 flex items-center justify-between">
{% block chat_header_actions %}
{% endblock %}
</div>
</section>
<div id="chat-scroll-container" class="overflow-auto hide-scrollbar">
{% block chat_content %}
{% endblock %}
</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() {
if (!window.location.pathname.startsWith('/chat')) return;
requestAnimationFrame(doScrollChatToBottom);
}
window.scrollChatToBottom = scrollChatToBottom;
// Delay initial scroll to avoid interfering with view transition
document.addEventListener('DOMContentLoaded', () => setTimeout(scrollChatToBottom, 350));
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 %}

View File

@@ -1,81 +1,14 @@
{% extends 'body_base.html' %} {% extends "chat/_layout.html" %}
{% block title %}Minne - Chat{% endblock %} {% block chat_header_actions %}
<h1 class="text-xl font-extrabold tracking-tight">Chat</h1>
<div class="text-xs opacity-70">Converse with your knowledge</div>
{% endblock %}
{% block main %} {% block chat_content %}
<div class="flex grow relative justify-center mt-2 sm:mt-4"> {% include "chat/history.html" %}
<div class="container">
<section class="mb-3">
<div class="nb-panel p-3 flex items-center justify-between">
<h1 class="text-xl font-extrabold tracking-tight">Chat</h1>
<div class="text-xs opacity-70">Converse with your knowledge</div>
</div>
</section>
<div id="chat-scroll-container" class="overflow-auto hide-scrollbar">
{% include "chat/history.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() {
if (!window.location.pathname.startsWith('/chat')) return;
requestAnimationFrame(doScrollChatToBottom);
}
window.scrollChatToBottom = scrollChatToBottom;
// Delay initial scroll to avoid interfering with view transition
document.addEventListener('DOMContentLoaded', () => setTimeout(scrollChatToBottom, 350));
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 %} {% endblock %}
{% block overlay %} {% block overlay %}
{% include "chat/new_message_form.html" %} {% include "chat/new_message_form.html" %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,15 @@
{% macro icon(name) %}
{% if name == "home" %}
{% include "icons/home_icon.html" %}
{% elif name == "book" %}
{% include "icons/book_icon.html" %}
{% elif name == "document" %}
{% include "icons/document_icon.html" %}
{% elif name == "chat" %}
{% include "icons/chat_icon.html" %}
{% elif name == "search" %}
{% include "icons/search_icon.html" %}
{% elif name == "scratchpad" %}
{% include "icons/scratchpad_icon.html" %}
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,14 @@
<nav class="sticky top-0 z-10 nb-panel nb-panel-canvas border-t-0">
<div class="container mx-auto navbar">
<div class="mr-2 flex-1">
{% block navbar_search %}
{% endblock %}
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-2 gap-2 items-center">
{% block navbar_actions %}
{% endblock %}
</ul>
</div>
</div>
</nav>

View File

@@ -0,0 +1,60 @@
<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">
<!-- === TOP FIXED SECTION === -->
<div class="px-2 mt-4 space-y-3">
{% block sidebar_nav_items %}
{% endblock %}
</div>
<!-- === 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>
</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>
{% endif %}
</li>
{% endfor %}
{% else %}
{% endif %}
</div>
<!-- === BOTTOM FIXED SECTION === -->
<div class="px-2 pb-4 space-y-3">
{% block sidebar_bottom_actions %}
{% endblock %}
</div>
</ul>
</div>

View File

@@ -0,0 +1,17 @@
{% extends 'body_base.html' %}
{% block title %}Minne - Content{% endblock %}
{% block main %}
<main id="main_section" class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 w-full">
<div class="container">
<div class="nb-panel p-3 mb-4 flex items-center justify-between">
{% block content_header %}
{% endblock %}
</div>
{% block content_list %}
{% endblock %}
</div>
</main>
{% endblock %}

View File

@@ -1,29 +1,23 @@
{% extends 'body_base.html' %} {% extends 'content/_layout.html' %}
{% block title %}Minne - Content{% endblock %} {% block content_header %}
<h2 class="text-xl font-extrabold tracking-tight">Content</h2>
{% block main %} <form hx-get="/content" hx-target="#main_section" hx-swap="outerHTML" hx-push-url="true"
<main id="main_section" class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 w-full"> class="flex items-center gap-2 mt-2 sm:mt-0">
<div class="container"> <input type="hidden" name="page" value="1" />
<div class="nb-panel p-3 mb-4 flex items-center justify-between"> <div>
<h2 class="text-xl font-extrabold tracking-tight">Content</h2> <select name="category" class="nb-select">
<form hx-get="/content" hx-target="#main_section" hx-swap="outerHTML" hx-push-url="true" <option value="">All Categories</option>
class="flex items-center gap-2 mt-2 sm:mt-0"> {% for category in categories %}
<input type="hidden" name="page" value="1" /> <option value="{{ category }}" {% if selected_category==category %}selected{% endif %}>{{ category }}
<div> </option>
<select name="category" class="nb-select"> {% endfor %}
<option value="">All Categories</option> </select>
{% for category in categories %}
<option value="{{ category }}" {% if selected_category==category %}selected{% endif %}>{{ category }}
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="nb-btn btn-sm">Filter</button>
</form>
</div> </div>
<button type="submit" class="nb-btn btn-sm">Filter</button>
{% include "content/content_list.html" %} </form>
</div> {% endblock %}
</main>
{% block content_list %}
{% include "content/content_list.html" %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "dashboard/_layout.html" %}
{% block dashboard_header %}
<h1 class="text-xl font-extrabold tracking-tight">Dashboard</h1>
<button class="nb-btn nb-cta" hx-get="/ingress-form" hx-target="#modal" hx-swap="innerHTML">
{% include "icons/send_icon.html" %}
<span class="ml-2">Add Content</span>
</button>
{% endblock %}
{% block dashboard_widgets %}
{% include "dashboard/statistics.html" %}
{% include "dashboard/recent_content.html" %}
{% include "dashboard/active_jobs.html" %}
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "body_base.html" %}
{% block title %}Minne - Dashboard{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4 w-full">
<div class="container">
<section class="mb-4">
{% block dashboard_alerts %}
{% endblock %}
<div class="nb-panel p-3 flex items-center justify-between">
{% block dashboard_header %}
{% endblock %}
</div>
</section>
{% block dashboard_widgets %}
{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -1,26 +1 @@
{% extends "body_base.html" %} {% extends "dashboard/_base.html" %}
{% block title %}Minne - Dashboard{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4 w-full">
<div class="container">
<section class="mb-4">
<div class="nb-panel p-3 flex items-center justify-between">
<h1 class="text-xl font-extrabold tracking-tight">Dashboard</h1>
<button class="nb-btn nb-cta" hx-get="/ingress-form" hx-target="#modal" hx-swap="innerHTML">
{% include "icons/send_icon.html" %}
<span class="ml-2">Add Content</span>
</button>
</div>
</section>
{% include "dashboard/statistics.html" %}
{% include "dashboard/recent_content.html" %}
{% include "dashboard/active_jobs.html" %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="container justify-center flex-grow flex mx-auto mt-4">
<div class="flex flex-col space-y-4 text-center justify-center">
{% block error_content %}
{% endblock %}
</div>
</main>
{% endblock %}

View File

@@ -1,13 +1,10 @@
{% extends 'body_base.html' %} {% extends "errors/_layout.html" %}
{% block main %}
<main class="container justify-center flex-grow flex mx-auto mt-4"> {% block error_content %}
<div class="flex flex-col space-y-4 text-center justify-center"> <h1 class="text-2xl font-bold text-error">
<h1 class="text-2xl font-bold text-error"> {{ status_code }}
{{ status_code }} </h1>
</h1> <p class="text-2xl my-4">{{ title }}</p>
<p class="text-2xl my-4">{{ title }}</p> <p class="text-base-content/60">{{ description }}</p>
<p class="text-base-content/60">{{ description }}</p> <a href="/" class="btn btn-primary mt-8">Go Home</a>
<a href="/" class="btn btn-primary mt-8">Go Home</a> {% endblock %}
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends 'body_base.html' %}
{% block title %}Minne - Knowledge{% endblock %}
{% block main %}
<div id="knowledge_pane" class="flex justify-center grow mt-2 sm:mt-4 gap-6">
<div class="container">
<div class="nb-panel p-3 mb-4 space-y-3 sm:space-y-0 sm:flex sm:flex-row sm:justify-between sm:items-center">
{% block knowledge_header %}
{% endblock %}
</div>
{% block knowledge_content %}
{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -1,52 +1,46 @@
{% extends 'body_base.html' %} {% extends 'knowledge/_layout.html' %}
{% block title %}Minne - Knowledge{% endblock %} {% block knowledge_header %}
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
{% block main %} <h2 class="text-xl font-extrabold tracking-tight">Knowledge Entities</h2>
<div id="knowledge_pane" class="flex justify-center grow mt-2 sm:mt-4 gap-6"> <button type="button" class="nb-btn nb-cta btn-sm mr-2" hx-get="/knowledge-entity/new" hx-target="#modal"
<div class="container"> hx-swap="innerHTML">
<div class="nb-panel p-3 mb-4 space-y-3 sm:space-y-0 sm:flex sm:flex-row sm:justify-between sm:items-center"> New Entity
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"> </button>
<h2 class="text-xl font-extrabold tracking-tight">Knowledge Entities</h2>
<button type="button" class="nb-btn nb-cta btn-sm mr-2" hx-get="/knowledge-entity/new" hx-target="#modal"
hx-swap="innerHTML">
New Entity
</button>
</div>
<form hx-get="/knowledge" hx-target="#knowledge_pane" hx-push-url="true" hx-swap="outerHTML"
class="flex items-center gap-2 mt-2 sm:mt-0">
<input type="hidden" name="page" value="1" />
<div>
<select name="entity_type" class="nb-select">
<option value="">All Types</option>
{% for type in entity_types %}
<option value="{{ type }}" {% if selected_entity_type==type %}selected{% endif %}>{{ type }}</option>
{% endfor %}
</select>
</div>
<div>
<select name="content_category" class="nb-select">
<option value="">All Categories</option>
{% for category in content_categories %}
<option value="{{ category }}" {% if selected_content_category==category %}selected{% endif %}>{{ category
}}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="nb-btn btn-sm">Filter</button>
</form>
</div>
<h2 class="text-2xl font-bold mb-2 mt-10 ">Graph</h2>
<div class="nb-card mt-4 p-2">
<div id="knowledge-graph" class="w-full" style="height: 640px;"
data-entity-type="{{ selected_entity_type | default(value='') }}"
data-content-category="{{ selected_content_category | default(value='') }}">
</div>
</div>
{% include "knowledge/entity_list.html" %}
<h2 class="text-2xl font-bold mb-2 mt-2">Relationships</h2>
{% include "knowledge/relationship_table.html" %}
</div> </div>
</div> <form hx-get="/knowledge" hx-target="#knowledge_pane" hx-push-url="true" hx-swap="outerHTML"
{% endblock %} class="flex items-center gap-2 mt-2 sm:mt-0">
<input type="hidden" name="page" value="1" />
<div>
<select name="entity_type" class="nb-select">
<option value="">All Types</option>
{% for type in entity_types %}
<option value="{{ type }}" {% if selected_entity_type==type %}selected{% endif %}>{{ type }}</option>
{% endfor %}
</select>
</div>
<div>
<select name="content_category" class="nb-select">
<option value="">All Categories</option>
{% for category in content_categories %}
<option value="{{ category }}" {% if selected_content_category==category %}selected{% endif %}>{{ category
}}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="nb-btn btn-sm">Filter</button>
</form>
{% endblock %}
{% block knowledge_content %}
<h2 class="text-2xl font-bold mb-2 mt-10 ">Graph</h2>
<div class="nb-card mt-4 p-2">
<div id="knowledge-graph" class="w-full" style="height: 640px;"
data-entity-type="{{ selected_entity_type | default(value='') }}"
data-content-category="{{ selected_content_category | default(value='') }}">
</div>
</div>
{% include "knowledge/entity_list.html" %}
<h2 class="text-2xl font-bold mb-2 mt-2">Relationships</h2>
{% include "knowledge/relationship_table.html" %}
{% endblock %}

View File

@@ -1,15 +1,12 @@
<nav class="sticky top-0 z-10 nb-panel nb-panel-canvas border-t-0" style="view-transition-name: navbar; contain: layout;"> {% extends "components/_navbar_layout.html" %}
<div class="container mx-auto navbar">
<div class="mr-2 flex-1"> {% block navbar_search %}
{% include "searchbar.html" %} {% include "searchbar.html" %}
</div> {% endblock %}
<div class="flex-none">
<ul class="menu menu-horizontal px-2 gap-2 items-center"> {% block navbar_actions %}
<label for="my-drawer" aria-label="open sidebar" class="hover:cursor-pointer lg:hidden"> <label for="my-drawer" aria-label="open sidebar" class="hover:cursor-pointer lg:hidden">
{% include "icons/hamburger_icon.html" %} {% include "icons/hamburger_icon.html" %}
</label> </label>
{% include "theme_toggle.html" %} {% include "theme_toggle.html" %}
</ul> {% endblock %}
</div>
</div>
</nav>

View File

@@ -0,0 +1,24 @@
{% extends 'body_base.html' %}
{% block title %}Minne - Scratchpad{% endblock %}
{% block main %}
<main id="main_section" class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 w-full">
<div class="container">
{% block scratchpad_header %}
{% endblock %}
{% block scratchpad_content %}
{% endblock %}
{% block scratchpad_archived %}
{% endblock %}
</div>
</main>
{% if new_scratchpad %}
<div hx-swap-oob="innerHTML:#modal">
<div hx-get="/scratchpad/{{ new_scratchpad.id }}/modal" hx-trigger="load" hx-target="#modal" hx-swap="innerHTML"></div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,113 +1,101 @@
{% extends 'body_base.html' %} {% extends 'scratchpad/_layout.html' %}
{% block title %}Minne - Scratchpad{% endblock %} {% block scratchpad_header %}
<div class="nb-panel p-3 mb-4 flex items-center justify-between">
<h2 class="text-xl font-extrabold tracking-tight">Scratchpads</h2>
<form hx-post="/scratchpad" hx-target="#main_section" hx-swap="outerHTML" class="flex gap-2">
<input type="text" name="title" placeholder="Enter scratchpad title..." class="nb-input nb-input-sm" required>
<button type="submit" class="nb-btn nb-cta">
{% include "icons/scratchpad_icon.html" %} Create
</button>
</form>
</div>
{% endblock %}
{% block main %} {% block scratchpad_content %}
<main id="main_section" class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 w-full"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="container"> {% for scratchpad in scratchpads %}
{% block header %} <div class="nb-card p-4 hover:nb-shadow-hover transition-all">
<div class="nb-panel p-3 mb-4 flex items-center justify-between"> <div class="flex justify-between items-start mb-2">
<h2 class="text-xl font-extrabold tracking-tight">Scratchpads</h2> <h3 class="font-semibold text-lg truncate flex-1">{{ scratchpad.title }}</h3>
<form hx-post="/scratchpad" hx-target="#main_section" hx-swap="outerHTML" class="flex gap-2"> <div class="flex gap-1 ml-2">
<input type="text" name="title" placeholder="Enter scratchpad title..." class="nb-input nb-input-sm" required> <button hx-get="/scratchpad/{{ scratchpad.id }}/modal" hx-target="#modal" hx-swap="innerHTML"
class="nb-btn nb-btn-sm btn-ghost" title="Edit scratchpad">
{% include "icons/pencil_icon.html" %}
</button>
<form hx-post="/scratchpad/{{ scratchpad.id }}/archive" hx-target="#main_section" hx-swap="outerHTML"
class="inline-flex">
<button type="submit" class="nb-btn nb-btn-sm btn-ghost text-warning" title="Archive scratchpad">
{% include "icons/delete_icon.html" %}
</button>
</form>
</div>
</div>
<div class="text-sm text-base-content/70 mb-2">
{{ scratchpad.content[:100] }}{% if scratchpad.content|length > 100 %}...{% endif %}
</div>
<div class="text-xs text-base-content/50">
Last saved: {{ scratchpad.last_saved_at | datetimeformat(format="short", tz=user.timezone) }}
</div>
</div>
{% else %}
<div class="col-span-full nb-panel p-8 text-center">
<h3 class="text-lg font-semibold mt-2 mb-2">No scratchpads yet</h3>
<p class="text-base-content/70 mb-4">Create your first scratchpad to start jotting down ideas</p>
<form hx-post="/scratchpad" hx-target="#main_section" hx-swap="outerHTML"
class="inline-flex gap-2">
<input type="text" name="title" placeholder="My first scratchpad..." class="nb-input" required>
<button type="submit" class="nb-btn nb-cta"> <button type="submit" class="nb-btn nb-cta">
{% include "icons/scratchpad_icon.html" %} Create {% include "icons/scratchpad_icon.html" %} Create Scratchpad
</button> </button>
</form> </form>
</div> </div>
{% endblock %} {% endfor %}
</div>
{% endblock %}
{% block content %} {% block scratchpad_archived %}
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> {% if archived_scratchpads %}
{% for scratchpad in scratchpads %} <div class="mt-6">
<div class="nb-card p-4 hover:nb-shadow-hover transition-all"> <details class="nb-panel p-3 space-y-4">
<div class="flex justify-between items-start mb-2"> <summary class="flex items-center justify-between gap-2 text-sm font-semibold cursor-pointer">
<h3 class="font-semibold text-lg truncate flex-1">{{ scratchpad.title }}</h3> <span>Archived Scratchpads</span>
<div class="flex gap-1 ml-2"> <span class="nb-badge">{{ archived_scratchpads|length }}</span>
<button hx-get="/scratchpad/{{ scratchpad.id }}/modal" hx-target="#modal" hx-swap="innerHTML" </summary>
class="nb-btn nb-btn-sm btn-ghost" title="Edit scratchpad">
{% include "icons/pencil_icon.html" %}
</button>
<form hx-post="/scratchpad/{{ scratchpad.id }}/archive" hx-target="#main_section" hx-swap="outerHTML"
class="inline-flex">
<button type="submit" class="nb-btn nb-btn-sm btn-ghost text-warning" title="Archive scratchpad">
{% include "icons/delete_icon.html" %}
</button>
</form>
</div>
</div>
<div class="text-sm text-base-content/70 mb-2">
{{ scratchpad.content[:100] }}{% if scratchpad.content|length > 100 %}...{% endif %}
</div>
<div class="text-xs text-base-content/50">
Last saved: {{ scratchpad.last_saved_at | datetimeformat(format="short", tz=user.timezone) }}
</div>
</div>
{% else %}
<div class="col-span-full nb-panel p-8 text-center">
<h3 class="text-lg font-semibold mt-2 mb-2">No scratchpads yet</h3>
<p class="text-base-content/70 mb-4">Create your first scratchpad to start jotting down ideas</p>
<form hx-post="/scratchpad" hx-target="#main_section" hx-swap="outerHTML"
class="inline-flex gap-2">
<input type="text" name="title" placeholder="My first scratchpad..." class="nb-input" required>
<button type="submit" class="nb-btn nb-cta">
{% include "icons/scratchpad_icon.html" %} Create Scratchpad
</button>
</form>
</div>
{% endfor %}
</div>
{% endblock %}
{% if archived_scratchpads %} <div class="text-sm text-base-content/60">Archived scratchpads were ingested into your knowledge base. You can
<div class="mt-6"> restore them if you want to keep editing.</div>
<details class="nb-panel p-3 space-y-4">
<summary class="flex items-center justify-between gap-2 text-sm font-semibold cursor-pointer">
<span>Archived Scratchpads</span>
<span class="nb-badge">{{ archived_scratchpads|length }}</span>
</summary>
<div class="text-sm text-base-content/60">Archived scratchpads were ingested into your knowledge base. You can <div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
restore them if you want to keep editing.</div> {% for scratchpad in archived_scratchpads %}
<div class="nb-card p-3 space-y-3">
<div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3"> <div class="flex items-start justify-between gap-3">
{% for scratchpad in archived_scratchpads %} <div class="flex-1 min-w-0">
<div class="nb-card p-3 space-y-3"> <h4 class="font-semibold text-base truncate" title="{{ scratchpad.title }}">{{ scratchpad.title }}</h4>
<div class="flex items-start justify-between gap-3"> <div class="text-xs text-base-content/50">Archived {{ scratchpad.archived_at | datetimeformat(format="short", tz=user.timezone) }}</div>
<div class="flex-1 min-w-0"> {% if scratchpad.ingested_at %}
<h4 class="font-semibold text-base truncate" title="{{ scratchpad.title }}">{{ scratchpad.title }}</h4> <div class="text-xs text-base-content/40">Ingestion started {{ scratchpad.ingested_at | datetimeformat(format="short", tz=user.timezone) }}</div>
<div class="text-xs text-base-content/50">Archived {{ scratchpad.archived_at | datetimeformat(format="short", tz=user.timezone) }}</div> {% endif %}
{% if scratchpad.ingested_at %} </div>
<div class="text-xs text-base-content/40">Ingestion started {{ scratchpad.ingested_at | datetimeformat(format="short", tz=user.timezone) }}</div> <div class="flex items-center gap-2 flex-shrink-0 flex-wrap justify-end">
{% endif %} <form hx-post="/scratchpad/{{ scratchpad.id }}/restore" hx-target="#main_section" hx-swap="outerHTML"
</div> class="inline-flex">
<div class="flex items-center gap-2 flex-shrink-0 flex-wrap justify-end"> <button type="submit" class="nb-btn nb-btn-sm">
<form hx-post="/scratchpad/{{ scratchpad.id }}/restore" hx-target="#main_section" hx-swap="outerHTML" Restore
class="inline-flex"> </button>
<button type="submit" class="nb-btn nb-btn-sm"> </form>
Restore <form hx-delete="/scratchpad/{{ scratchpad.id }}" hx-target="#main_section" hx-swap="outerHTML"
</button> hx-confirm="Permanently delete this scratchpad?" class="inline-flex">
</form> <button type="submit" class="nb-btn nb-btn-sm btn-ghost text-error" title="Delete permanently">
<form hx-delete="/scratchpad/{{ scratchpad.id }}" hx-target="#main_section" hx-swap="outerHTML" {% include "icons/delete_icon.html" %}
hx-confirm="Permanently delete this scratchpad?" class="inline-flex"> </button>
<button type="submit" class="nb-btn nb-btn-sm btn-ghost text-error" title="Delete permanently"> </form>
{% include "icons/delete_icon.html" %}
</button>
</form>
</div>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
</details> {% endfor %}
</div> </div>
{% endif %} </details>
</div> </div>
</main> {% endif %}
{% if new_scratchpad %}
<div hx-swap-oob="innerHTML:#modal">
<div hx-get="/scratchpad/{{ new_scratchpad.id }}/modal" hx-trigger="load" hx-target="#modal" hx-swap="innerHTML"></div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "body_base.html" %}
{% block title %}Minne - Search{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4">
<div class="container">
<section class="mb-4">
<div class="nb-panel p-3 flex items-center justify-between">
{% block search_header %}
{% endblock %}
</div>
</section>
{% block search_results %}
{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -1,17 +1,10 @@
{% extends "body_base.html" %} {% extends "search/_layout.html" %}
{% block title %}Minne - Search{% endblock %} {% block search_header %}
<h1 class="text-xl font-extrabold tracking-tight">Search</h1>
{% block main %} <div class="text-xs opacity-70">Find documents, entities, and snippets</div>
<div class="flex justify-center grow mt-2 sm:mt-4"> {% endblock %}
<div class="container">
<section class="mb-4"> {% block search_results %}
<div class="nb-panel p-3 flex items-center justify-between"> {% include "search/response.html" %}
<h1 class="text-xl font-extrabold tracking-tight">Search</h1>
<div class="text-xs opacity-70">Find documents, entities, and snippets</div>
</div>
</section>
{% include "search/response.html" %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,119 +1,52 @@
{% macro icon(name) %} {% extends "components/_sidebar_layout.html" %}
{% if name == "home" %} {% from "components/_icon_macro.html" import icon %}
{% include "icons/home_icon.html" %}
{% elif name == "book" %}
{% include "icons/book_icon.html" %}
{% elif name == "document" %}
{% include "icons/document_icon.html" %}
{% elif name == "chat" %}
{% include "icons/chat_icon.html" %}
{% elif name == "search" %}
{% include "icons/search_icon.html" %}
{% elif name == "scratchpad" %}
{% include "icons/scratchpad_icon.html" %}
{% endif %}
{% endmacro %}
<div class="drawer-side z-20" style="view-transition-name: sidebar; contain: layout;"> {% block sidebar_nav_items %}
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label> {% for url, name, label in [
("/", "home", "Dashboard"),
("/knowledge", "book", "Knowledge"),
("/content", "document", "Content"),
("/chat", "chat", "Chat"),
("/search", "search", "Search"),
("/scratchpad", "scratchpad", "Scratchpad")
] %}
<li>
<a hx-boost="true" href="{{ url }}" class="nb-btn w-full justify-start gap-3 bg-base-100 hover:bg-base-200">
{{ icon(name) }}
<span class="uppercase tracking-wide">{{ label }}</span>
</a>
</li>
{% endfor %}
<li>
<button class="nb-btn nb-cta w-full flex items-center gap-3 justify-start mt-2" hx-get="/ingress-form"
hx-target="#modal" hx-swap="innerHTML">{% include "icons/send_icon.html" %} Add
Content</button>
</li>
<div class="u-hairline mt-4"></div>
{% endblock %}
<ul class="menu p-0 w-72 h-full nb-canvas text-base-content flex flex-col border-r-2 border-neutral"> {% block sidebar_bottom_actions %}
<!-- <a class="px-4 py-4 text-2xl font-extrabold tracking-tight text-primary border-b-2 border-neutral bg-base-100 nb-shadow" --> <li>
<!-- href="/" hx-boost="true">Minne</a> --> <a hx-boost="true" href="/account"
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200">
<!-- === TOP FIXED SECTION === --> {% include "icons/user_icon.html" %}
<div class="px-2 mt-4 space-y-3"> <span class="uppercase tracking-wide">Account</span>
{% for url, name, label in [ </a>
("/", "home", "Dashboard"), </li>
("/knowledge", "book", "Knowledge"), {% if user.admin %}
("/content", "document", "Content"), <li>
("/chat", "chat", "Chat"), <a hx-boost="true" href="/admin"
("/search", "search", "Search"), class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200">
("/scratchpad", "scratchpad", "Scratchpad") {% include "icons/wrench_screwdriver_icon.html" %}
] %} <span class="uppercase tracking-wide">Admin</span>
<li> </a>
<a hx-boost="true" href="{{ url }}" class="nb-btn w-full justify-start gap-3 bg-base-100 hover:bg-base-200"> </li>
{{ icon(name) }} {% endif %}
<span class="uppercase tracking-wide">{{ label }}</span> <li>
</a> <a hx-boost="true" href="/signout"
</li> class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200 border-error text-error">
{% endfor %} {% include "icons/logout_icon.html" %}
<li> <span class="uppercase tracking-wide">Logout</span>
<button class="nb-btn nb-cta w-full flex items-center gap-3 justify-start mt-2" hx-get="/ingress-form" </a>
hx-target="#modal" hx-swap="innerHTML">{% include "icons/send_icon.html" %} Add </li>
Content</button> {% endblock %}
</li>
<div class="u-hairline mt-4"></div>
</div>
<!-- === 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>
</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>
{% endif %}
</li>
{% endfor %}
{% else %}
{% endif %}
</div>
<!-- === BOTTOM FIXED SECTION === -->
<div class="px-2 pb-4 space-y-3">
<li>
<a hx-boost="true" href="/account"
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200">
{% include "icons/user_icon.html" %}
<span class="uppercase tracking-wide">Account</span>
</a>
</li>
{% if user.admin %}
<li>
<a hx-boost="true" href="/admin"
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200">
{% include "icons/wrench_screwdriver_icon.html" %}
<span class="uppercase tracking-wide">Admin</span>
</a>
</li>
{% endif %}
<li>
<a hx-boost="true" href="/signout"
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200 border-error text-error">
{% include "icons/logout_icon.html" %}
<span class="uppercase tracking-wide">Logout</span>
</a>
</li>
</div>
</ul>
</div>

View File

@@ -116,6 +116,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
config.clone(), config.clone(),
reranker_pool.clone(), reranker_pool.clone(),
embedding_provider.clone(), embedding_provider.clone(),
None,
) )
.await?; .await?;
@@ -284,6 +285,7 @@ mod tests {
config.clone(), config.clone(),
None, None,
embedding_provider, embedding_provider,
None,
) )
.await .await
.expect("failed to build html state"); .expect("failed to build html state");

View File

@@ -71,6 +71,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
config.clone(), config.clone(),
reranker_pool, reranker_pool,
embedding_provider, embedding_provider,
None,
) )
.await?; .await?;