1 Commits

Author SHA1 Message Date
Per Stark
8fe4ac9fec release: 1.0.0 2026-01-11 18:37:07 +01:00
36 changed files with 584 additions and 750 deletions

View File

@@ -2,7 +2,7 @@
**A graph-powered personal knowledge base that makes storing easy.** **A graph-powered personal knowledge base that makes storing easy.**
Capture content effortlessly, let AI discover connections, and explore your knowledge through search, chat and visually. Self-hosted and privacy-focused. Capture content effortlessly, let AI discover connections, and explore your knowledge visually. Self-hosted and privacy-focused.
[![Release Status](https://github.com/perstarkse/minne/actions/workflows/release.yml/badge.svg)](https://github.com/perstarkse/minne/actions/workflows/release.yml) [![Release Status](https://github.com/perstarkse/minne/actions/workflows/release.yml/badge.svg)](https://github.com/perstarkse/minne/actions/workflows/release.yml)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)

View File

@@ -21,49 +21,19 @@ pub enum TemplateEngine {
#[macro_export] #[macro_export]
macro_rules! create_template_engine { macro_rules! create_template_engine {
// Single path argument // Macro takes the relative path to the templates dir as input
($relative_path:expr) => { ($relative_path:expr) => {{
$crate::create_template_engine!($relative_path, Option::<&str>::None)
};
// Path + Fallback argument
($relative_path:expr, $fallback_path:expr) => {{
// Code for debug builds (AutoReload) // Code for debug builds (AutoReload)
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
// These lines execute in the CALLING crate's context // These lines execute in the CALLING crate's context
let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let template_path = crate_dir.join($relative_path); let template_path = crate_dir.join($relative_path);
let fallback_path = $fallback_path.map(|p| crate_dir.join(p));
let reloader = $crate::utils::template_engine::AutoReloader::new(move |notifier| { let reloader = $crate::utils::template_engine::AutoReloader::new(move |notifier| {
let mut env = $crate::utils::template_engine::Environment::new(); let mut env = $crate::utils::template_engine::Environment::new();
env.set_loader($crate::utils::template_engine::path_loader(&template_path));
let loader_primary = $crate::utils::template_engine::path_loader(&template_path);
// Clone fallback_path for the closure
let fallback = fallback_path.clone();
env.set_loader(move |name| match loader_primary(name) {
Ok(Some(tmpl)) => Ok(Some(tmpl)),
Ok(None) => {
if let Some(ref fb_path) = fallback {
let loader_fallback =
$crate::utils::template_engine::path_loader(fb_path);
loader_fallback(name)
} else {
Ok(None)
}
}
Err(e) => Err(e),
});
notifier.set_fast_reload(true); notifier.set_fast_reload(true);
notifier.watch_path(&template_path, true); notifier.watch_path(&template_path, true);
if let Some(ref fb) = fallback_path {
notifier.watch_path(fb, true);
}
// Add contrib filters/functions // Add contrib filters/functions
$crate::utils::template_engine::minijinja_contrib::add_to_environment(&mut env); $crate::utils::template_engine::minijinja_contrib::add_to_environment(&mut env);
Ok(env) Ok(env)

View File

@@ -12,13 +12,11 @@ include = ["lib"]
# The installers to generate for each app # The installers to generate for each app
installers = [] installers = []
# Target platforms to build apps for (Rust target-triple syntax) # Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] targets = ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
# Skip checking whether the specified configuration files are up to date # Skip checking whether the specified configuration files are up to date
allow-dirty = ["ci"] allow-dirty = ["ci"]
[dist.github-custom-runners] [dist.github-custom-runners]
aarch64-apple-darwin = "macos-latest"
x86_64-apple-darwin = "macos-15-intel"
x86_64-unknown-linux-gnu = "ubuntu-22.04" x86_64-unknown-linux-gnu = "ubuntu-22.04"
x86_64-unknown-linux-musl = "ubuntu-22.04" x86_64-unknown-linux-musl = "ubuntu-22.04"
x86_64-pc-windows-msvc = "windows-latest" x86_64-pc-windows-msvc = "windows-latest"

View File

@@ -29,17 +29,15 @@ 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 templates = let template_engine = create_template_engine!("templates");
template_engine.unwrap_or_else(|| Arc::new(create_template_engine!("templates"))); debug!("Template engine created for html_router.");
debug!("Template engine configured for html_router.");
Ok(Self { Ok(Self {
db, db,
openai_client, openai_client,
session_store, session_store,
templates, templates: Arc::new(template_engine),
config, config,
storage, storage,
reranker_pool, reranker_pool,

View File

@@ -172,7 +172,7 @@ pub struct ModelSettingsInput {
processing_model: String, processing_model: String,
image_processing_model: String, image_processing_model: String,
voice_processing_model: String, voice_processing_model: String,
embedding_model: Option<String>, embedding_model: String,
embedding_dimensions: Option<u32>, embedding_dimensions: Option<u32>,
} }
@@ -219,9 +219,7 @@ pub async fn update_model_settings(
.embedding_dimensions .embedding_dimensions
.is_some_and(|new_dims| new_dims != current_settings.embedding_dimensions); .is_some_and(|new_dims| new_dims != current_settings.embedding_dimensions);
( (
input input.embedding_model,
.embedding_model
.unwrap_or_else(|| current_settings.embedding_model.clone()),
input input
.embedding_dimensions .embedding_dimensions
.unwrap_or(current_settings.embedding_dimensions), .unwrap_or(current_settings.embedding_dimensions),

View File

@@ -1,20 +0,0 @@
{% 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

@@ -1,29 +0,0 @@
{% 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 +1,38 @@
{% extends "admin/_base.html" %} {% 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">
<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

@@ -1,72 +0,0 @@
{% 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

@@ -1,10 +0,0 @@
{% 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

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

View File

@@ -1,8 +1,9 @@
{% extends "auth/_layout.html" %} {% extends "head_base.html" %}
{% block title %}Minne - Sign up{% endblock %} {% block title %}Minne - Sign up{% endblock %}
{% block auth_content %} {% block body %}
<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">
@@ -45,9 +46,10 @@
</div> </div>
</div> </div>
</div> </div>
<script> </div>
// Detect timezone and set hidden input <script>
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect timezone and set hidden input
document.getElementById("timezone").value = timezone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
</script> document.getElementById("timezone").value = timezone;
</script>
{% endblock %} {% endblock %}

View File

@@ -1,78 +0,0 @@
{% 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,14 +1,81 @@
{% extends "chat/_layout.html" %} {% extends 'body_base.html' %}
{% block chat_header_actions %} {% block title %}Minne - Chat{% endblock %}
<h1 class="text-xl font-extrabold tracking-tight">Chat</h1>
<div class="text-xs opacity-70">Converse with your knowledge</div>
{% endblock %}
{% block chat_content %} {% block main %}
{% include "chat/history.html" %} <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">
<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

@@ -1,15 +0,0 @@
{% 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

@@ -1,14 +0,0 @@
<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

@@ -1,60 +0,0 @@
<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

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

View File

@@ -1,15 +0,0 @@
{% 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

@@ -1,21 +0,0 @@
{% 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 +1,26 @@
{% extends "dashboard/_base.html" %} {% 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">
<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

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

View File

@@ -1,17 +0,0 @@
{% 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,46 +1,52 @@
{% extends 'knowledge/_layout.html' %} {% extends 'body_base.html' %}
{% block knowledge_header %} {% block title %}Minne - Knowledge{% endblock %}
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<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>
{% endblock %}
{% block knowledge_content %} {% block main %}
<h2 class="text-2xl font-bold mb-2 mt-10 ">Graph</h2> <div id="knowledge_pane" class="flex justify-center grow mt-2 sm:mt-4 gap-6">
<div class="nb-card mt-4 p-2"> <div class="container">
<div id="knowledge-graph" class="w-full" style="height: 640px;" <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">
data-entity-type="{{ selected_entity_type | default(value='') }}" <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
data-content-category="{{ selected_content_category | default(value='') }}"> <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> </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>
{% include "knowledge/entity_list.html" %} </div>
<h2 class="text-2xl font-bold mb-2 mt-2">Relationships</h2> {% endblock %}
{% include "knowledge/relationship_table.html" %}
{% endblock %}

View File

@@ -1,12 +1,15 @@
{% extends "components/_navbar_layout.html" %} <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">
{% block navbar_search %} <div class="mr-2 flex-1">
{% include "searchbar.html" %} {% include "searchbar.html" %}
{% endblock %} </div>
<div class="flex-none">
{% block navbar_actions %} <ul class="menu menu-horizontal px-2 gap-2 items-center">
<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" %}
{% endblock %} </ul>
</div>
</div>
</nav>

View File

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

View File

@@ -1,18 +0,0 @@
{% 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,10 +1,17 @@
{% extends "search/_layout.html" %} {% extends "body_base.html" %}
{% block search_header %} {% block title %}Minne - Search{% endblock %}
<h1 class="text-xl font-extrabold tracking-tight">Search</h1>
<div class="text-xs opacity-70">Find documents, entities, and snippets</div>
{% endblock %}
{% block search_results %} {% block main %}
{% include "search/response.html" %} <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">
<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,52 +1,119 @@
{% extends "components/_sidebar_layout.html" %} {% macro icon(name) %}
{% from "components/_icon_macro.html" import icon %} {% 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 %}
{% block sidebar_nav_items %} <div class="drawer-side z-20" style="view-transition-name: sidebar; contain: layout;">
{% for url, name, label in [ <label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
("/", "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 %}
{% block sidebar_bottom_actions %} <ul class="menu p-0 w-72 h-full nb-canvas text-base-content flex flex-col border-r-2 border-neutral">
<li> <!-- <a class="px-4 py-4 text-2xl font-extrabold tracking-tight text-primary border-b-2 border-neutral bg-base-100 nb-shadow" -->
<a hx-boost="true" href="/account" <!-- href="/" hx-boost="true">Minne</a> -->
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200">
{% include "icons/user_icon.html" %} <!-- === TOP FIXED SECTION === -->
<span class="uppercase tracking-wide">Account</span> <div class="px-2 mt-4 space-y-3">
</a> {% for url, name, label in [
</li> ("/", "home", "Dashboard"),
{% if user.admin %} ("/knowledge", "book", "Knowledge"),
<li> ("/content", "document", "Content"),
<a hx-boost="true" href="/admin" ("/chat", "chat", "Chat"),
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200"> ("/search", "search", "Search"),
{% include "icons/wrench_screwdriver_icon.html" %} ("/scratchpad", "scratchpad", "Scratchpad")
<span class="uppercase tracking-wide">Admin</span> ] %}
</a> <li>
</li> <a hx-boost="true" href="{{ url }}" class="nb-btn w-full justify-start gap-3 bg-base-100 hover:bg-base-200">
{% endif %} {{ icon(name) }}
<li> <span class="uppercase tracking-wide">{{ label }}</span>
<a hx-boost="true" href="/signout" </a>
class="nb-btn w-full justify-start items-center gap-3 bg-base-100 hover:bg-base-200 border-error text-error"> </li>
{% include "icons/logout_icon.html" %} {% endfor %}
<span class="uppercase tracking-wide">Logout</span> <li>
</a> <button class="nb-btn nb-cta w-full flex items-center gap-3 justify-start mt-2" hx-get="/ingress-form"
</li> hx-target="#modal" hx-swap="innerHTML">{% include "icons/send_icon.html" %} Add
{% endblock %} Content</button>
</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,7 +116,6 @@ 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?;
@@ -285,7 +284,6 @@ 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,7 +71,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
config.clone(), config.clone(),
reranker_pool, reranker_pool,
embedding_provider, embedding_provider,
None,
) )
.await?; .await?;