design: neobrutalist_theme into main

This commit is contained in:
Per Stark
2025-09-17 10:00:55 +02:00
parent 62d909bb7e
commit 6ea51095e8
57 changed files with 1791 additions and 951 deletions

View File

@@ -1,207 +1,156 @@
{% extends 'body_base.html' %}
{% block title %}Minne - Account{% endblock %}
{% block title %}Minne - Admin{% endblock %}
{% block main %}
<main class="container flex-grow flex flex-col mx-auto mt-4 space-y-6">
<h1 class="text-2xl font-bold mb-2">Admin Dashboard</h1>
<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">Admin Dashboard</h1>
</div>
</section>
<div class="stats stats-vertical md:stats-horizontal shadow">
<div class="stat">
<div class="stat-title font-bold">Page loads</div>
<div class="stat-value text-secondary">{{analytics.page_loads}}</div>
<div class="stat-desc">Amount of page loads</div>
</div>
<section class="mb-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="nb-stat">
<div class="text-xs opacity-70">Page Loads</div>
<div class="text-3xl font-extrabold">{{analytics.page_loads}}</div>
<div class="text-xs opacity-60">Total page load events</div>
</div>
<div class="nb-stat">
<div class="text-xs opacity-70">Unique Visitors</div>
<div class="text-3xl font-extrabold">{{analytics.visitors}}</div>
<div class="text-xs opacity-60">Distinct users by fingerprint</div>
</div>
<div class="nb-stat">
<div class="text-xs opacity-70">Users</div>
<div class="text-3xl font-extrabold">{{users}}</div>
<div class="text-xs opacity-60">Registered accounts</div>
</div>
</div>
</section>
<div class="stat">
<div class="stat-title font-bold">Unique visitors</div>
<div class="stat-value text-primary">{{analytics.visitors}}</div>
<div class="stat-desc">Amount of unique visitors</div>
</div>
<div class="stat">
<div class="stat-title font-bold">Users</div>
<div class="stat-value text-accent">{{users}}</div>
<div class="stat-desc">Amount of registered users</div>
</div>
</div>
<!-- Settings in Fieldset -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
{% block system_prompt_section %}
<div id="system_prompt_section">
<fieldset class="fieldset p-4 shadow rounded-box">
<legend class="fieldset-legend">System Prompts</legend>
<section class="grid grid-cols-1 xl:grid-cols-2 gap-4">
{% block system_prompt_section %}
<div id="system_prompt_section" class="nb-panel p-4">
<div class="text-sm font-semibold mb-3">System Prompts</div>
<div class="flex gap-2 flex-col sm:flex-row">
<button type="button" class="btn btn-primary btn-sm" hx-get="/edit-query-prompt" hx-target="#modal"
hx-swap="innerHTML">
Edit Query Prompt
</button>
<button type="button" class="btn btn-primary btn-sm" hx-get="/edit-ingestion-prompt" hx-target="#modal"
hx-swap="innerHTML">
Edit Ingestion Prompt
</button>
<button type="button" class="btn btn-primary btn-sm" hx-get="/edit-image-prompt" hx-target="#modal"
hx-swap="innerHTML">
Edit Image Prompt
</button>
<button type="button" class="nb-btn btn-sm" hx-get="/edit-query-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Query Prompt</button>
<button type="button" class="nb-btn btn-sm" hx-get="/edit-ingestion-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Ingestion Prompt</button>
<button type="button" class="nb-btn btn-sm" hx-get="/edit-image-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Image Prompt</button>
</div>
</fieldset>
</div>
{% endblock %}
<fieldset class="fieldset p-4 shadow rounded-box">
<legend class="fieldset-legend">AI Models</legend>
{% block model_settings_form %}
<form hx-patch="/update-model-settings" hx-swap="outerHTML">
<!-- Query Model -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Query Model</span>
</label>
<select name="query_model" class="select select-bordered w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.query_model==model.id %} selected {% endif %}>{{model.id}}
</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 mt-1">
Current used:
<span class="font-mono">{{settings.query_model}}</span>
</p>
</div>
<!-- Processing Model -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Processing Model</span>
</label>
<select name="processing_model" class="select select-bordered w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.processing_model==model.id %} selected {% endif %}>{{model.id}}
</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 mt-1">
Current used:
<span class="font-mono">{{settings.processing_model}}</span>
</p>
</div>
<!-- Image Processing Model -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Image Processing Model</span>
</label>
<select name="image_processing_model" class="select select-bordered w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.image_processing_model==model.id %} selected {% endif %}>
{{model.id}}
</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 mt-1">
Current used:
<span class="font-mono">{{settings.image_processing_model}}</span>
</p>
</div>
<!-- Voice Processing Model -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Voice Processing Model</span>
</label>
<select name="voice_processing_model" class="select select-bordered w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.voice_processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 mt-1">
Current used:
<span class="font-mono">{{settings.voice_processing_model}}</span>
</p>
</div>
<!-- Embedding Model -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Embedding Model</span>
</label>
<select name="embedding_model" class="select select-bordered w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.embedding_model==model.id %} selected {% endif %}>{{model.id}}
</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 mt-1">
Current used:
<span class="font-mono">{{settings.embedding_model}} ({{settings.embedding_dimensions}} dims)</span>
</p>
</div>
<!-- Embedding Dimensions (Always Visible) -->
<div class="form-control mb-4">
<label class="label" for="embedding_dimensions">
<span class="label-text">Embedding Dimensions</span>
</label>
<input type="number" id="embedding_dimensions" name="embedding_dimensions" class="input input-bordered w-full"
value="{{ settings.embedding_dimensions }}" required />
</div>
<!-- Conditional Alert -->
<div id="embedding-change-alert" role="alert" class="alert alert-warning mt-2 hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span><strong>Warning:</strong> Changing dimensions will require re-creating all embeddings. Make sure you
look up what dimensions the model uses or use a model that allows specifying embedding dimensions</span>
</div>
<button type="submit" class="btn btn-primary btn-sm mt-4">Save Model Settings</button>
</form>
<script>
// Use a self-executing function to avoid polluting the global scope
// and to ensure it runs correctly after an HTMX swap.
(() => {
const dimensionInput = document.getElementById('embedding_dimensions');
const alertElement = document.getElementById('embedding-change-alert');
// The initial value is read directly from the template each time this script runs.
const initialDimensions = '{{ settings.embedding_dimensions }}';
if (dimensionInput && alertElement) {
// Use the 'input' event for immediate feedback as the user types.
dimensionInput.addEventListener('input', (event) => {
// Show alert if the current value is not the initial value. Hide it otherwise.
if (event.target.value !== initialDimensions) {
alertElement.classList.remove('hidden');
} else {
alertElement.classList.add('hidden');
}
});
}
})();
</script>
</div>
{% endblock %}
</fieldset>
<fieldset class="fieldset p-4 shadow rounded-box">
<legend class="fieldset-legend">Registration</legend>
<label class="flex gap-4 text-center">
{% block registration_status_input %}
<form hx-patch="/toggle-registrations" hx-swap="outerHTML" hx-trigger="change">
<input name="registration_open" type="checkbox" class="checkbox" {% if settings.registrations_enabled
%}checked{% endif %} />
<div class="nb-panel p-4">
<div class="text-sm font-semibold mb-3">AI Models</div>
{% block model_settings_form %}
<form hx-patch="/update-model-settings" hx-swap="outerHTML" class="grid grid-cols-1 gap-4">
<!-- Query Model -->
<div>
<div class="text-sm opacity-80 mb-1">Query Model</div>
<select name="query_model" class="nb-select w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.query_model==model.id %} selected {% endif %}>{{model.id}}</option>
{% endfor %}
</select>
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.query_model}}</span></p>
</div>
<!-- Processing Model -->
<div>
<div class="text-sm opacity-80 mb-1">Processing Model</div>
<select name="processing_model" class="nb-select w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
{% endfor %}
</select>
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.processing_model}}</span></p>
</div>
<!-- Image Processing Model -->
<div>
<div class="text-sm opacity-80 mb-1">Image Processing Model</div>
<select name="image_processing_model" class="nb-select w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.image_processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
{% endfor %}
</select>
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.image_processing_model}}</span></p>
</div>
<!-- Voice Processing Model -->
<div>
<div class="text-sm opacity-80 mb-1">Voice Processing Model</div>
<select name="voice_processing_model" class="nb-select w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.voice_processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
{% endfor %}
</select>
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.voice_processing_model}}</span></p>
</div>
<!-- Embedding Model -->
<div>
<div class="text-sm opacity-80 mb-1">Embedding Model</div>
<select name="embedding_model" class="nb-select w-full">
{% for model in available_models.data %}
<option value="{{model.id}}" {% if settings.embedding_model==model.id %} selected {% endif %}>{{model.id}}</option>
{% endfor %}
</select>
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.embedding_model}} ({{settings.embedding_dimensions}} dims)</span></p>
</div>
<!-- Embedding Dimensions -->
<div>
<div class="text-sm opacity-80 mb-1" for="embedding_dimensions">Embedding Dimensions</div>
<input type="number" id="embedding_dimensions" name="embedding_dimensions" class="nb-input w-full" value="{{ settings.embedding_dimensions }}" required />
</div>
<!-- Alert -->
<div id="embedding-change-alert" class="nb-panel p-3 bg-warning/20 hidden">
<div class="text-sm"><strong>Warning:</strong> Changing dimensions will require re-creating all embeddings. Look up your model's required dimensions or use a model that allows specifying them.</div>
</div>
<div class="flex justify-end">
<button type="submit" class="nb-btn nb-cta btn-sm">Save Model Settings</button>
</div>
</form>
{% endblock %}
Enable Registrations
</label>
<div id="registration-status" class="text-sm mt-2"></div>
</fieldset>
<script>
// Rebind after HTMX swaps
(() => {
const dimensionInput = document.getElementById('embedding_dimensions');
const alertElement = document.getElementById('embedding-change-alert');
const initialDimensions = '{{ settings.embedding_dimensions }}';
if (dimensionInput && alertElement) {
dimensionInput.addEventListener('input', (event) => {
if (String(event.target.value) !== String(initialDimensions)) {
alertElement.classList.remove('hidden');
} else {
alertElement.classList.add('hidden');
}
});
}
})();
</script>
{% endblock %}
</div>
<div class="nb-panel p-4">
<div class="text-sm font-semibold mb-3">Registration</div>
<label class="flex items-center gap-3">
{% block registration_status_input %}
<form hx-patch="/toggle-registrations" hx-swap="outerHTML" hx-trigger="change">
<input name="registration_open" type="checkbox" class="nb-checkbox" {% if settings.registrations_enabled %}checked{% endif %} />
</form>
{% endblock %}
<span class="text-sm">Enable Registrations</span>
</label>
<div id="registration-status" class="text-xs opacity-70 mt-2"></div>
</div>
</section>
</div>
</main>
{% endblock %}
</div>
{% endblock %}

View File

@@ -7,17 +7,17 @@ hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold mb-4">Edit Image Processing Prompt</h3>
<h3 class="text-xl font-extrabold tracking-tight mb-2">Edit Image Processing Prompt</h3>
<div class="form-control">
<textarea name="image_processing_prompt" class="textarea textarea-bordered h-96 w-full font-mono text-sm">{{
<textarea name="image_processing_prompt" class="nb-input h-96 w-full font-mono text-sm">{{
settings.image_processing_prompt }}</textarea>
<p class="text-xs text-gray-500 mt-1">System prompt used for processing images</p>
<p class="text-xs opacity-70 mt-1">System prompt used for processing images</p>
</div>
{% endblock %}
{% block primary_actions %}
<button type="button" class="btn btn-outline mr-2" id="reset_prompt_button">
<button type="button" class="nb-btn mr-2" id="reset_prompt_button">
Reset to Default
</button>
@@ -29,10 +29,10 @@ hx-swap="outerHTML"
});
</script>
<button type="submit" class="btn btn-primary">
<button type="submit" class="nb-btn nb-cta">
<span class="htmx-indicator hidden">
<span class="loading loading-spinner loading-xs mr-2"></span>
</span>
Save Changes
</button>
{% endblock %}
{% endblock %}

View File

@@ -7,17 +7,17 @@ hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold mb-4">Edit Ingestion Prompt</h3>
<h3 class="text-xl font-extrabold tracking-tight mb-2">Edit Ingestion Prompt</h3>
<div class="form-control">
<textarea name="ingestion_system_prompt" class="textarea textarea-bordered h-96 w-full font-mono text-sm">{{
<textarea name="ingestion_system_prompt" class="nb-input h-96 w-full font-mono text-sm">{{
settings.ingestion_system_prompt }}</textarea>
<p class="text-xs text-gray-500 mt-1">System prompt used for content processing and ingestion</p>
<p class="text-xs opacity-70 mt-1">System prompt used for content processing and ingestion</p>
</div>
{% endblock %}
{% block primary_actions %}
<button type="button" class="btn btn-outline mr-2" id="reset_prompt_button">
<button type="button" class="nb-btn mr-2" id="reset_prompt_button">
Reset to Default
</button>
@@ -29,10 +29,10 @@ hx-swap="outerHTML"
});
</script>
<button type="submit" class="btn btn-primary">
<button type="submit" class="nb-btn nb-cta">
<span class="htmx-indicator hidden">
<span class="loading loading-spinner loading-xs mr-2"></span>
</span>
Save Changes
</button>
{% endblock %}
{% endblock %}

View File

@@ -7,17 +7,17 @@ hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold mb-4">Edit System Prompt</h3>
<h3 class="text-xl font-extrabold tracking-tight mb-2">Edit System Prompt</h3>
<div class="form-control">
<textarea name="query_system_prompt" class="textarea textarea-bordered h-96 w-full font-mono text-sm">{{
<textarea name="query_system_prompt" class="nb-input h-96 w-full font-mono text-sm">{{
settings.query_system_prompt }}</textarea>
<p class="text-xs text-gray-500 mt-1">System prompt used for answering user queries</p>
<p class="text-xs opacity-70 mt-1">System prompt used for answering user queries</p>
</div>
{% endblock %}
{% block primary_actions %}
<button type="button" class="btn btn-outline mr-2" id="reset_prompt_button">
<button type="button" class="nb-btn mr-2" id="reset_prompt_button">
Reset to Default
</button>
@@ -29,10 +29,10 @@ hx-swap="outerHTML"
});
</script>
<button type="submit" class="btn btn-primary">
<button type="submit" class="nb-btn nb-cta">
<span class="htmx-indicator hidden">
<span class="loading loading-spinner loading-xs mr-2"></span>
</span>
Save Changes
</button>
{% endblock %}
{% endblock %}

View File

@@ -3,91 +3,86 @@
{% block title %}Minne - Account{% endblock %}
{% block main %}
<style>
form.htmx-request {
opacity: 0.5;
}
</style>
<main class="container flex-grow flex flex-col mx-auto mt-4 space-y-1">
<h1 class="text-2xl font-bold mb-2">Account Settings</h1>
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" value="{{ user.email }}" class="input text-primary-content input-bordered w-full"
disabled />
</div>
<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>
<div class="form-control">
<label class="label">
<span class="label-text">API key</span>
</label>
{% 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="input text-primary-content input-bordered w-full pr-12" disabled />
<button type="button" id="copy_api_key_btn" onclick="copy_api_key()"
class="absolute inset-y-0 cursor-pointer right-0 flex items-center pr-3" title="Copy API key">
{% include "icons/clipboard_icon.html" %}
</button>
</div>
<a href="https://www.icloud.com/shortcuts/66985f7b98a74aaeac6ba29c3f1f0960"
class="btn btn-accent mt-4 w-full">Download iOS shortcut</a>
{% else %}
<button hx-post="/set-api-key" class="btn btn-secondary w-full" hx-swap="outerHTML">
Create API-Key
</button>
{% endif %}
{% endblock %}
</div>
<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>
<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">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>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Timezone</span>
</label>
{% block timezone_section %}
<select name="timezone" class="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 %}
</div>
<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>
<div class="form-control mt-4 hidden">
<button hx-post="/verify-email" class="btn btn-secondary w-full">
Verify Email
</button>
<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 class="form-control mt-4">
{% block change_password_section %}
<button hx-get="/change-password" hx-swap="outerHTML" class="btn btn-primary w-full">
Change Password
</button>
{% endblock %}
</div>
<div class="form-control mt-4">
<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="btn btn-error w-full">
Delete Account
</button>
</div>
<div id="account-result" class="mt-4"></div>
</main>
{% endblock %}
</div>
{% endblock %}

View File

@@ -1,5 +1,12 @@
<form hx-patch="/change-password" class="flex flex-col gap-1">
<input name="old_password" class="input w-full" type="password" placeholder="Enter old password"></input>
<input name="new_password" class="input w-full" type="password" placeholder="Enter new password"></input>
<button class="btn btn-primary w-full">Change Password</button>
</form>
<form hx-patch="/change-password" class="flex flex-col gap-3">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Old Password</div>
<input name="old_password" class="nb-input w-full" type="password" placeholder="Enter old password"></input>
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">New Password</div>
<input name="new_password" class="nb-input w-full" type="password" placeholder="Enter new password"></input>
</label>
<button class="nb-btn w-full">Change Password</button>
</form>

View File

@@ -1,52 +1,43 @@
<style>
form.htmx-request {
opacity: 0.5;
}
</style>
<div class="flex justify-center grow container mx-auto px-4 sm:px-0 sm:max-w-md flex-col">
<h1
class="text-5xl sm:text-6xl py-4 pt-10 font-bold bg-linear-to-r from-primary to-secondary text-center text-transparent bg-clip-text">
Minne
</h1>
<h2 class="text-2xl font-bold text-center mb-8">Login to your account</h2>
<form hx-post="/signin" hx-target="#login-result">
<div class="form-control">
<label class="floating-label">
<span>Email</span>
<input name="email" type="email" placeholder="Email" class="input input-md w-full validator" required />
<div class="validator-hint hidden">Enter valid email address</div>
</label>
<div class="container mx-auto px-4 sm:max-w-md flex-1 flex items-center justify-center">
<div class="w-full nb-card p-5">
<div class="flex items-center justify-between mb-3">
<div class="brand-mark text-3xl font-extrabold tracking-tight">MINNE</div>
<span class="nb-badge">Sign In</span>
</div>
<div class="u-hairline mb-3"></div>
<div class="form-control mt-4">
<label class="floating-label">
<span>Password</span>
<input name="password" type="password" class="input validator w-full" required placeholder="Password"
<form hx-post="/signin" hx-target="#login-result" class="flex flex-col gap-2">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Email</div>
<input name="email" type="email" placeholder="Email" class="nb-input w-full validator" required />
<div class="validator-hint hidden text-xs opacity-70 mt-1">Enter valid email address</div>
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Password</div>
<input name="password" type="password" class="nb-input w-full validator" required placeholder="Password"
minlength="8" />
</div>
<div class="form-control mt-4">
<label class="label cursor-pointer justify-start gap-4">
<input type="checkbox" name="remember_me" class="checkbox " />
<span class="label-text">Remember me</span>
</label>
<div class="mt-1 text-error" id="login-result"></div>
<div class="form-control mt-1">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" name="remember_me" class="nb-checkbox" />
<span class="label-text">Remember me</span>
</label>
</div>
<div class="form-control mt-1">
<button id="submit-btn" class="nb-btn nb-cta w-full">Login</button>
</div>
</form>
<div class="u-hairline my-3"></div>
<div class="text-center text-sm">
Dont have an account?
<a href="/signup" hx-boost="true" class="nb-link">Sign up</a>
</div>
<div class="mt-4" id="login-result"></div>
<div class="form-control mt-6">
<button id="submit-btn" class="btn btn-primary w-full">
Login
</button>
</div>
</form>
<div class="divider">OR</div>
<div class="text-center text-sm">
Don't have an account?
<a href="/signup" hx-boost="true" class="link link-primary">Sign up</a>
</div>
</div>
</div>

View File

@@ -3,56 +3,48 @@
{% block title %}Minne - Sign up{% endblock %}
{% block body %}
<style>
form.htmx-request {
opacity: 0.5;
}
</style>
<div class="min-h-[100dvh] flex items-center">
<div class="container mx-auto px-4 sm:max-w-md">
<div class="nb-card p-5">
<div class="flex items-center justify-between mb-3">
<div class="text-3xl font-extrabold tracking-tight">MINNE</div>
<span class="nb-badge">Sign Up</span>
</div>
<div class="u-hairline mb-3"></div>
<div class="min-h-[100dvh] container mx-auto px-4 sm:px-0 sm:max-w-md flex justify-center flex-col">
<h1
class="text-5xl sm:text-6xl py-4 pt-10 font-bold bg-linear-to-r from-primary to-secondary text-center text-transparent bg-clip-text">
Minne
</h1>
<h2 class="text-2xl font-bold text-center mb-8">Create your account</h2>
<form hx-post="/signup" hx-target="#signup-result" class="flex flex-col gap-4">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Email</div>
<input type="email" placeholder="Email" name="email" required class="nb-input w-full validator" />
<div class="validator-hint hidden text-xs opacity-70 mt-1">Enter valid email address</div>
</label>
<form hx-post="/signup" hx-target="#signup-result" class="">
<div class="form-control">
<label class="floating-label">
<span>Email</span>
<input type="email" placeholder="Email" name="email" required class="input input-md w-full validator" />
<div class="validator-hint hidden">Enter valid email address</div>
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Password</div>
<input type="password" name="password" class="nb-input w-full validator" required placeholder="Password"
minlength="8" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="Must be more than 8 characters, including number, lowercase letter, uppercase letter" />
<p class="validator-hint hidden text-xs opacity-70 mt-1">
Must be more than 8 characters, including
<br />At least one number
<br />At least one lowercase letter
<br />At least one uppercase letter
</p>
</label>
<div class="mt-2 text-error" id="signup-result"></div>
<div class="form-control mt-1">
<button id="submit-btn" class="nb-btn nb-cta w-full">Create Account</button>
</div>
<input type="hidden" name="timezone" id="timezone" />
</form>
<div class="u-hairline my-3"></div>
<div class="text-center text-sm">
Already have an account?
<a href="/signin" hx-boost="true" class="nb-link">Sign in</a>
</div>
</div>
<div class="form-control mt-4">
<label class="floating-label">
<span>Password</span>
<input type="password" name="password" class="input validator w-full" required placeholder="Password"
minlength="8" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="Must be more than 8 characters, including number, lowercase letter, uppercase letter" />
<p class="validator-hint hidden">
Must be more than 8 characters, including
<br />At least one number
<br />At least one lowercase letter
<br />At least one uppercase letter
</p>
</label>
</div>
<div class="mt-4 text-error" id="signup-result"></div>
<div class="form-control mt-6">
<button id="submit-btn" class="btn btn-primary w-full">
Create Account
</button>
</div>
<input type="hidden" name="timezone" id="timezone" />
</form>
<div class="divider">OR</div>
<div class="text-center text-sm">
Already have an account?
<a href="/signin" hx-boost="true" class="link link-primary">Sign in</a>
</div>
</div>
<script>
@@ -60,4 +52,4 @@
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.getElementById("timezone").value = timezone;
</script>
{% endblock %}
{% endblock %}

View File

@@ -2,7 +2,7 @@
{% block body %}
<body class="bg-base-100 relative" hx-ext="head-support">
<body class="relative" hx-ext="head-support">
<div class="drawer lg:drawer-open">
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
<!-- Page Content -->
@@ -10,8 +10,9 @@
<!-- Navbar -->
{% include "navigation_bar.html" %}
<!-- Main Content Area -->
<main class="flex flex-1 overflow-y-auto">
<main class="flex flex-col flex-1 overflow-y-auto">
{% block main %}{% endblock %}
<div class="p32 min-h-[10px]"></div>
</main>
</div>
<!-- Sidebar -->
@@ -21,25 +22,5 @@
</div> <!-- End Drawer -->
<div id="modal"></div>
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
<!-- Add CSS for custom scrollbar -->
<style>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
</style>
</body>
{% endblock %}

View File

@@ -9,94 +9,40 @@
{% block main %}
<div class="flex grow relative justify-center mt-2 sm:mt-4">
<div class="container">
<div class="overflow-auto hide-scrollbar">
<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" %}
{% include "chat/new_message_form.html" %}
</div>
</div>
</div>
<style>
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.markdown-content p {
margin-bottom: 0.75em;
}
.markdown-content p:last-child {
margin-bottom: 0;
}
.markdown-content ul,
.markdown-content ol {
margin-top: 0.5em;
margin-bottom: 0.75em;
padding-left: 2em;
}
.markdown-content li {
margin-bottom: 0.25em;
}
.markdown-content pre {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.5em;
border-radius: 4px;
overflow-x: auto;
}
.markdown-content code {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
.markdown-content {
line-height: 1.5;
word-wrap: break-word;
}
.markdown-content table {
border-collapse: collapse;
margin: 0.75em 0;
width: 100%;
}
.markdown-content th,
.markdown-content td {
border: 1px solid #ddd;
padding: 6px 12px;
text-align: left;
}
.markdown-content blockquote {
border-left: 4px solid #ddd;
padding-left: 10px;
margin: 0.5em 0 0.5em 0.5em;
color: #666;
}
.markdown-content hr {
border: none;
border-top: 1px solid #ddd;
margin: 0.75em 0;
}
</style>
<script>
function scrollChatToBottom() {
const chatContainer = document.getElementById('chat_container');
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
requestAnimationFrame(() => {
const mainScroll = document.querySelector('main');
if (mainScroll) mainScroll.scrollTop = mainScroll.scrollHeight;
const chatScroll = document.getElementById('chat-scroll-container');
if (chatScroll) chatScroll.scrollTop = chatScroll.scrollHeight;
const chatContainer = document.getElementById('chat_container');
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
});
}
window.scrollChatToBottom = scrollChatToBottom;
document.addEventListener('DOMContentLoaded', scrollChatToBottom);
document.body.addEventListener('htmx:afterSwap', scrollChatToBottom);
document.body.addEventListener('htmx:afterSettle', scrollChatToBottom);
</script>
{% endblock %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
<div id="chat_container" class="pl-3 overflow-y-auto h-[calc(100vh-170px)] sm:h-[calc(100vh-190px)] hide-scrollbar">
<div id="chat_container" class="px-3 pb-44 space-y-3">
{% for message in history %}
{% if message.role == "AI" %}
<div class="chat chat-start">

View File

@@ -1,11 +1,9 @@
{% include "chat/streaming_response.html" %}
<!-- OOB swap targeting the form element directly -->
<form id="chat-form" hx-post="/chat/{{conversation.id}}" hx-target="#chat_container" hx-swap="beforeend"
class="relative flex gap-2" hx-swap-oob="true">
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
class="textarea textarea-ghost rounded-2xl rounded-b-none h-24 sm:rounded-b-2xl pr-8 bg-base-200 flex-grow resize-none"
id="chat-input"></textarea>
class="nb-input h-24 pr-8 pl-2 pt-2 pb-2 flex-grow resize-none" id="chat-input"></textarea>
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-1">
{% include "icons/send_icon.html" %}
</button>

View File

@@ -1,13 +1,14 @@
<div class="absolute w-full mx-auto max-w-3xl p-0 pb-0 sm:pb-4 left-0 right-0 bottom-0 z-10">
<form hx-post="{% if conversation %} /chat/{{conversation.id}} {% else %} /chat {% endif %}"
hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2" id="chat-form">
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"
class="textarea textarea-ghost rounded-2xl rounded-b-none h-24 sm:rounded-b-2xl pr-8 bg-base-200 flex-grow resize-none focus:outline-none focus:bg-base-200"
id="chat-input"></textarea>
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-6">{% include
"icons/send_icon.html" %}
</button>
</form>
<div class="fixed bottom-0 left-0 right-0 lg:left-72 z-20">
<div class="mx-auto max-w-3xl px-4 pb-3">
<div class="nb-panel p-2">
<form hx-post="{% if conversation %} /chat/{{conversation.id}} {% else %} /chat {% endif %}"
hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2 items-end" id="chat-form">
<textarea autofocus required name="content" placeholder="Type your message…" rows="3"
class="nb-input flex-grow min-h-24 pr-10 pl-2 pt-2 pb-2 resize-none" id="chat-input"></textarea>
<button type="submit" class="nb-btn nb-cta h-10 px-3">{% include "icons/send_icon.html" %}</button>
</form>
</div>
</div>
</div>
<script>
@@ -23,4 +24,4 @@
document.getElementById('chat-input').value = ''; // Clear the textarea
}
});
</script>
</script>

View File

@@ -1,8 +1,8 @@
<div class="relative my-2">
<button id="references-toggle-{{message.id}}"
class="text-xs text-blue-500 hover:text-blue-700 hover:underline focus:outline-none flex items-center"
class="nb-btn btn-xs bg-base-100 hover:bg-base-200 flex items-center"
onclick="toggleReferences('{{message.id}}')">
References
REFERENCES
{% include "icons/chevron_icon.html" %}
</button>
<div id="references-content-{{message.id}}" class="hidden max-w-full mt-1">
@@ -10,7 +10,7 @@
{% for reference in message.references %}
<div class="reference-badge-container" data-reference="{{reference}}" data-message-id="{{message.id}}"
data-index="{{loop.index}}">
<span class="badge badge-xs badge-neutral truncate max-w-[20ch] overflow-hidden text-left block cursor-pointer">
<span class="nb-badge truncate max-w-[20ch] overflow-hidden text-left block cursor-pointer">
{{reference}}
</span>
</div>
@@ -80,7 +80,7 @@
function createTooltip() {
const tooltip = document.createElement('div');
tooltip.id = tooltipId;
tooltip.className = 'fixed z-[9999] bg-neutral-800 text-white p-3 rounded-md shadow-lg text-sm w-72 max-w-xs border border-neutral-700 hidden';
tooltip.className = 'reference-tooltip hidden';
tooltip.innerHTML = '<div class="animate-pulse">Loading...</div>';
document.body.appendChild(tooltip);
return tooltip;
@@ -135,15 +135,3 @@
});
}
</script>
<style>
#references-toggle- {
{
message.id
}
}
svg {
transition: transform 0.2s ease;
}
</style>

View File

@@ -25,7 +25,7 @@
e.preventDefault();
window.markdownBuffer[msgId] = (window.markdownBuffer[msgId] || '') + (e.detail.data || '');
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
if (typeof scrollChatToBottom === "function") scrollChatToBottom();
if (typeof window.scrollChatToBottom === "function") window.scrollChatToBottom();
});
document.body.addEventListener('htmx:sseClose', function () {
const msgId = '{{ user_message.id }}';
@@ -33,7 +33,7 @@
if (el && window.markdownBuffer[msgId]) {
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
delete window.markdownBuffer[msgId];
if (typeof scrollChatToBottom === "function") scrollChatToBottom();
if (typeof window.scrollChatToBottom === "function") window.scrollChatToBottom();
}
});
</script>
</script>

View File

@@ -3,14 +3,14 @@
{% 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">
<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="flex items-center justify-between mb-4">
<h2 class="text-2xl font-bold">Content</h2>
<div class="nb-panel p-3 mb-4 flex items-center justify-between">
<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"
class="flex items-center gap-2 mt-2 sm:mt-0">
<div class="form-control">
<select name="category" class="select select-bordered">
<div>
<select name="category" class="nb-select">
<option value="">All Categories</option>
{% for category in categories %}
<option value="{{ category }}" {% if selected_category==category %}selected{% endif %}>{{ category }}
@@ -18,13 +18,11 @@
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm">Filter</button>
<button type="submit" class="nb-btn btn-sm">Filter</button>
</form>
</div>
<div id="text_content_cards">
{% include "content/content_list.html" %}
</div>
{% include "content/content_list.html" %}
</div>
</main>
{% endblock %}

View File

@@ -1,19 +1,19 @@
<div class="columns-1 md:columns-2 2xl:columns-3 gap-4" id="text_content_cards">
<div class="nb-masonry w-full" id="text_content_cards">
{% for text_content in text_contents %}
<div class="card cursor-pointer mb-4 bg-base-100 shadow break-inside-avoid-column"
<article class="nb-card cursor-pointer mx-auto mb-4 w-full max-w-[92vw] space-y-3 sm:max-w-none"
hx-get="/content/{{ text_content.id }}/read" hx-target="#modal" hx-swap="innerHTML">
{% if text_content.url_info %}
<figure>
<img src="/file/{{text_content.url_info.image_id}}" alt="website screenshot" />
<figure class="-mx-4 -mt-4 border-b-2 border-neutral bg-base-200">
<img class="w-full h-auto" src="/file/{{text_content.url_info.image_id}}" alt="website screenshot" />
</figure>
{% endif %}
{% if text_content.file_info.mime_type == "image/png" or text_content.file_info.mime_type == "image/jpeg" %}
<figure>
<img src="/file/{{text_content.file_info.id}}" alt="{{text_content.file_info.file_name}}" />
<figure class="-mx-4 -mt-4 border-b-2 border-neutral bg-base-200">
<img class="w-full h-auto" src="/file/{{text_content.file_info.id}}" alt="{{text_content.file_info.file_name}}" />
</figure>
{% endif %}
<div class="card-body max-w-[95vw]">
<h2 class="card-title truncate">
<div class="space-y-3 break-words">
<h2 class="text-lg font-extrabold tracking-tight truncate">
{% if text_content.url_info %}
{{text_content.url_info.title}}
{% elif text_content.file_info %}
@@ -22,37 +22,36 @@
{{text_content.text}}
{% endif %}
</h2>
<div class="flex items-center justify-between">
<p class="text-xs opacity-60">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="text-xs opacity-60 shrink-0">
{{ text_content.created_at | datetimeformat(format="short", tz=user.timezone) }}
</p>
<div class="badge badge-soft badge-secondary mr-2">{{ text_content.category }}</div>
<span class="nb-badge">{{ text_content.category }}</span>
<div class="flex gap-2" hx-on:click="event.stopPropagation()">
{% if text_content.url_info %}
<button class="btn-btn-square btn-ghost btn-sm">
<a href="{{text_content.url_info.url}}" target="_blank" rel="noopener noreferrer">
{% include "icons/link_icon.html" %}
</a>
</button>
<a href="{{text_content.url_info.url}}" target="_blank" rel="noopener noreferrer"
class="nb-btn btn-square btn-sm" aria-label="Open source link">
{% include "icons/link_icon.html" %}
</a>
{% endif %}
<button hx-get="/content/{{ text_content.id }}/read" hx-target="#modal" hx-swap="innerHTML"
class="btn btn-square btn-ghost btn-sm">
class="nb-btn btn-square btn-sm" aria-label="Read content">
{% include "icons/read_icon.html" %}
</button>
<button hx-get="/content/{{ text_content.id }}" hx-target="#modal" hx-swap="innerHTML"
class="btn btn-square btn-ghost btn-sm">
class="nb-btn btn-square btn-sm" aria-label="Edit content">
{% include "icons/edit_icon.html" %}
</button>
<button hx-delete="/content/{{ text_content.id }}" hx-target="#text_content_cards" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
class="nb-btn btn-square btn-sm" aria-label="Delete content">
{% include "icons/delete_icon.html" %}
</button>
</div>
</div>
<p class="mt-2">
<p class="text-sm leading-relaxed">
{{ text_content.instructions }}
</p>
</div>
</div>
</article>
{% endfor %}
</div>

View File

@@ -8,34 +8,49 @@ flex flex-col min-h-[95%] w-11/12 max-w-[90ch] max-h-[95%]
hx-patch="/content/{{text_content.id}}"
hx-target="#main_section"
hx-swap="outerHTML"
class="flex flex-col flex-1 h-full"
class="flex flex-col flex-1 h-full min-h-0"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold">Edit Content</h3>
<div class="form-control">
<label class="floating-label">
<span class="label-text">Context</span>
<input type="text" name="context" value="{{ text_content.context }}" class="w-full input input-bordered">
</label>
</div>
<div class="form-control">
<label class="floating-label">
<span class="label-text">Category</span>
<input type="text" name="category" value="{{ text_content.category }}" class="w-full input input-bordered">
</label>
</div>
<div class="form-control flex-1 flex flex-col min-h-0">
<label class="floating-label flex-1 flex flex-col min-h-0">
<span class="label-text">Text</span>
<textarea name="text" class="textarea textarea-bordered w-full flex-1 min-h-[200px] h-full resize-none">{{
text_content.text }}</textarea>
<h3 class="text-xl font-extrabold tracking-tight">Edit Content</h3>
<div class="flex flex-col gap-3 flex-1 min-h-0">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Context</div>
<input type="text" name="context" value="{{ text_content.context }}" class="nb-input w-full">
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Category</div>
<input type="text" name="category" value="{{ text_content.category }}" class="nb-input w-full">
</label>
<label class="w-full flex-1 flex flex-col min-h-0">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Text</div>
<textarea name="text" class="nb-input w-full flex-1 min-h-0 h-full resize-none overflow-y-auto">{{ text_content.text
}}</textarea>
</label>
</div>
<script>
(function () {
const form = document.getElementById('modal_form');
if (!form) return;
if (document.getElementById('main_section')) {
form.setAttribute('hx-target', '#main_section');
form.setAttribute('hx-swap', 'outerHTML');
return;
}
if (document.getElementById('latest_content_section')) {
form.setAttribute('hx-target', '#latest_content_section');
form.setAttribute('hx-swap', 'outerHTML');
return;
}
form.removeAttribute('hx-target');
form.setAttribute('hx-swap', 'none');
})();
</script>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
</button>
{% endblock %}
<button type="submit" class="nb-btn nb-cta">Save Changes</button>
{% endblock %}

View File

@@ -4,14 +4,14 @@
{% block modal_content %}
{% if text_content.url_info.image_id %}
<img class="rounded-t-md overflow-clip" src="/file/{{text_content.url_info.image_id}}" alt="Screenshot of the site" />
<img class="w-full border-b-2 border-neutral" src="/file/{{text_content.url_info.image_id}}" alt="Screenshot of the site" />
{% endif %}
{% if text_content.file_info.mime_type == "image/png" or text_content.file_info.mime_type == "image/jpeg" %}
<figure>
<img src="/file/{{text_content.file_info.id}}" alt="{{text_content.file_info.file_name}}" />
</figure>
{% endif %}
<div id="reader-{{text_content.id}}" class="markdown-content prose" data-content="{{text_content.text | escape }}">
<div id="reader-{{text_content.id}}" class="markdown-content prose-tufte" data-content="{{text_content.text | escape }}">
{{text_content.text | escape }}
</div>
@@ -39,4 +39,4 @@
{% endblock %}
{% block primary_actions %}
{% endblock %}
{% endblock %}

View File

@@ -1,49 +1,63 @@
{% block active_jobs_section %}
<ul id="active_jobs_section" class="list">
<div class="flex items-center gap-4">
<li class="py-4 text-2xl font-bold tracking-wide">Active Tasks</li>
<button class="cursor-pointer scale-75" hx-get="/active-jobs" hx-target="#active_jobs_section" hx-swap="outerHTML">
<section id="active_jobs_section" class="nb-panel p-4 space-y-4 mt-6 sm:mt-8">
<header class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-xl font-extrabold tracking-tight">Active Tasks</h2>
<button class="nb-btn btn-square btn-sm" hx-get="/active-jobs" hx-target="#active_jobs_section" hx-swap="outerHTML"
aria-label="Refresh active tasks">
{% include "icons/refresh_icon.html" %}
</button>
</div>
{% for item in active_jobs %}
<li class="list-row">
<div class="bg-secondary rounded-box size-10 flex justify-center items-center text-secondary-content">
{% if item.content.Url %}
{% include "icons/link_icon.html" %}
{% elif item.content.File %}
{% include "icons/document_icon.html" %}
{% else %}
{% include "icons/bars_icon.html" %}
{% endif %}
</div>
<div>
<div class="[&:before]:content-['Status:_'] [&:before]:opacity-60">
{% if item.status.name == "InProgress" %}
In Progress, attempt {{item.status.attempts}}
{% elif item.status.name == "Error" %}
Error: {{item.status.message}}
{% else %}
{{item.status.name}}
{% endif %}
</header>
{% if active_jobs %}
<ul class="flex flex-col gap-3 list-none p-0 m-0">
{% for item in active_jobs %}
<li class="nb-panel p-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div class="size-10 shrink-0 flex items-center justify-center border-2 border-neutral bg-transparent">
{% if item.content.Url %}
{% include "icons/link_icon.html" %}
{% elif item.content.File %}
{% include "icons/document_icon.html" %}
{% else %}
{% include "icons/bars_icon.html" %}
{% endif %}
</div>
<div class="space-y-1">
<div class="text-sm font-semibold">
{% if item.status.name == "InProgress" %}
In progress, attempt {{ item.status.attempts }}
{% elif item.status.name == "Error" %}
Error: {{ item.status.message }}
{% else %}
{{ item.status.name }}
{% endif %}
</div>
<div class="text-xs font-semibold opacity-60">
{{ item.created_at|datetimeformat(format="short", tz=user.timezone) }}
</div>
</div>
</div>
<div class="text-xs font-semibold opacity-60">
{{item.created_at|datetimeformat(format="short", tz=user.timezone)}} </div>
</div>
<p class="list-col-wrap text-xs [&:before]:content-['Content:_'] [&:before]:opacity-60">
{% if item.content.Url %}
{{item.content.Url.url}}
{% elif item.content.File %}
{{item.content.File.file_info.file_name}}
{% else %}
{{item.content.Text.text}}
{% endif %}
</p>
<button hx-delete="/jobs/{{item.id}}" hx-target="#active_jobs_section" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/delete_icon.html" %}
</button>
</li>
{% endfor %}
</ul>
<div class="sm:flex-1 sm:text-right">
<p class="text-xs opacity-80 leading-snug break-words">
{% if item.content.Url %}
{{ item.content.Url.url }}
{% elif item.content.File %}
{{ item.content.File.file_info.file_name }}
{% else %}
{{ item.content.Text.text }}
{% endif %}
</p>
</div>
<div class="flex items-center justify-end gap-2">
<button hx-delete="/jobs/{{ item.id }}" hx-target="#active_jobs_section" hx-swap="outerHTML"
class="nb-btn btn-square btn-sm" aria-label="Cancel task">
{% include "icons/delete_icon.html" %}
</button>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
{% endblock %}

View File

@@ -7,8 +7,20 @@
{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4">
<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" %}

View File

@@ -1,32 +1,40 @@
{% for task in tasks %}
<li class="list-row" hx-ext="sse" sse-connect="/task/status-stream?task_id={{task.id}}" sse-close="close_stream">
<div class="bg-secondary rounded-box size-10 flex justify-center items-center text-secondary-content"
sse-swap="stop_loading" hx-swap="innerHTML">
<span class="loading loading-spinner loading-xl"></span>
</div>
<div>
<div class="flex gap-1">
<div sse-swap="status" hx-swap="innerHTML">
Created
</div>
<div hx-get="/content/recent" hx-target="#latest_content_section" hx-swap="outerHTML"
hx-trigger="sse:update_latest_content"></div>
<li class="nb-panel p-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"
hx-ext="sse" sse-connect="/task/status-stream?task_id={{task.id}}" sse-close="close_stream">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div class="size-10 flex items-center justify-center border-2 border-neutral bg-transparent"
sse-swap="stop_loading" hx-swap="innerHTML">
<span class="loading loading-spinner loading-md"></span>
</div>
<div class="space-y-1">
<div class="text-sm font-semibold flex gap-2 items-center">
<span sse-swap="status" hx-swap="innerHTML">Created</span>
<div hx-get="/content/recent" hx-target="#latest_content_section" hx-swap="outerHTML"
hx-trigger="sse:update_latest_content"></div>
</div>
<div class="text-xs font-semibold opacity-60">
{{task.created_at|datetimeformat(format="short", tz=user.timezone)}}
</div>
</div>
<div class="text-xs font-semibold opacity-60">
{{task.created_at|datetimeformat(format="short", tz=user.timezone)}} </div>
</div>
<p class="list-col-wrap text-xs [&:before]:content-['Content:_'] [&:before]:opacity-60">
{% if task.content.Url %}
{{task.content.Url.url}}
{% elif task.content.File %}
{{task.content.File.file_info.file_name}}
{% else %}
{{task.content.Text.text}}
{% endif %}
</p>
<button hx-delete="/jobs/{{task.id}}" hx-target="#active_jobs_section" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/delete_icon.html" %}
</button>
<div class="sm:flex-1 sm:text-right">
<p class="text-xs opacity-80 leading-snug break-words">
{% if task.content.Url %}
{{task.content.Url.url}}
{% elif task.content.File %}
{{task.content.File.file_info.file_name}}
{% else %}
{{task.content.Text.text}}
{% endif %}
</p>
</div>
<div class="flex items-center justify-end gap-2">
<button hx-delete="/jobs/{{task.id}}" hx-target="#active_jobs_section" hx-swap="outerHTML"
class="nb-btn btn-square btn-sm" aria-label="Cancel task">
{% include "icons/delete_icon.html" %}
</button>
</div>
</li>
{% endfor %}
{% endfor %}

View File

@@ -0,0 +1,25 @@
<section class="mb-4 sm:mt-4">
<h2 class="text-2xl font-extrabold tracking-tight mb-3">Overview</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<div class="nb-stat">
<div class="text-xs opacity-70">Total Documents</div>
<div class="text-3xl font-extrabold">{{ stats.total_documents }}</div>
<div class="text-xs opacity-60">+{{ stats.new_documents_week }} this week</div>
</div>
<div class="nb-stat">
<div class="text-xs opacity-70">Text Chunks</div>
<div class="text-3xl font-extrabold">{{ stats.total_text_chunks }}</div>
<div class="text-xs opacity-60">+{{ stats.new_text_chunks_week }} this week</div>
</div>
<div class="nb-stat">
<div class="text-xs opacity-70">Knowledge Entities</div>
<div class="text-3xl font-extrabold">{{ stats.total_entities }}</div>
<div class="text-xs opacity-60">+{{ stats.new_entities_week }} this week</div>
</div>
<div class="nb-stat">
<div class="text-xs opacity-70">Conversations</div>
<div class="text-3xl font-extrabold">{{ stats.total_conversations }}</div>
<div class="text-xs opacity-60">+{{ stats.new_conversations_week }} this week</div>
</div>
</div>
</section>

View File

@@ -4,39 +4,37 @@ hx-post="/ingress-form"
enctype="multipart/form-data"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold">Add new content</h3>
<div class="form-control">
<label class="floating-label">
<span>Content</span>
<textarea name="content" class="textarea input-bordered w-full"
placeholder="Enter the content you want to ingest, it can be an URL or a text snippet">{{ content }}</textarea>
<h3 class="text-xl font-extrabold tracking-tight">Add New Content</h3>
<div class="flex flex-col gap-3">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Content</div>
<textarea name="content" class="nb-input w-full min-h-28"
placeholder="Paste a URL or type/paste text to ingest…">{{ content }}</textarea>
</label>
</div>
<div class="form-control">
<label class="floating-label">
<span>Context</span>
<textarea name="context" class="textarea w-full"
placeholder="Enter context for the AI here, help it understand what its seeing or how it should relate to the database">{{
context }}</textarea>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Context</div>
<textarea name="context" class="nb-input w-full min-h-24"
placeholder="Optional: add context to guide how the content should be interpreted…">{{ context }}</textarea>
</label>
</div>
<div class="form-control">
<label class="floating-label">
<span>Category</span>
<input type="text" name="category" class="input input-bordered validator w-full" value="{{ category }}"
list="category-list" required />
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Category</div>
<input type="text" name="category" class="nb-input validator w-full" value="{{ category }}" list="category-list" required />
<datalist id="category-list">
{% for category in user_categories %}
<option value="{{ category }}" />
{% endfor %}
</datalist>
<div class="validator-hint hidden">Category is required</div>
<div class="validator-hint hidden text-xs opacity-70 mt-1">Category is required</div>
</label>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Files</div>
<input type="file" name="files" multiple class="file-input w-full rounded-none border-2 border-neutral" />
</label>
</div>
<div class="form-control">
<label class="label label-text">Files</label>
<input type="file" name="files" multiple class="file-input file-input-bordered w-full" />
</div>
<div id="error-message" class="text-error text-center {% if not error %}hidden{% endif %}">{{ error }}</div>
<script>
(function () {
@@ -54,7 +52,7 @@ enctype="multipart/form-data"
</script>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
<button type="submit" class="nb-btn nb-cta">
Add Content
</button>
{% endblock %}
{% endblock %}

View File

@@ -3,22 +3,22 @@
{% 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 overflow-y-auto">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4">
<h2 class="text-2xl font-bold">Knowledge Entities</h2>
<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 flex flex-col sm:flex-row justify-between items-start sm:items-center">
<h2 class="text-xl font-extrabold tracking-tight">Knowledge Entities</h2>
<form hx-get="/knowledge" hx-target="#knowledge_pane" hx-push-url="true" hx-swap="outerHTML"
class="flex items-center gap-4 mt-2 sm:mt-0">
<div class="form-control">
<select name="entity_type" class="select select-bordered">
class="flex items-center gap-2 mt-2 sm:mt-0">
<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 class="form-control">
<select name="content_category" class="select select-bordered">
<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
@@ -26,12 +26,12 @@
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm">Filter</button>
<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="rounded-box overflow-clip mt-4 shadow p-2 mb-10">
<div class="nb-card mt-4 p-2 mb-30">
<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='') }}">

View File

@@ -7,18 +7,18 @@ hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold">Edit Entity</h3>
<h3 class="text-xl font-extrabold tracking-tight">Edit Entity</h3>
<div class="form-control">
<label class="floating-label">
<span class="label-text">Name</span>
<input type="text" name="name" value="{{ entity.name }}" class="input input-bordered w-full">
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Name</div>
<input type="text" name="name" value="{{ entity.name }}" class="nb-input w-full">
</label>
</div>
<div class="form-control relative" style="margin-top: -1.5rem;">
<div class="absolute !left-3 !top-2.5 z-50 p-0.5 bg-white text-xs text-light">Type</div>
<select name="entity_type" class="select w-full">
<div class="absolute !left-3 !top-2.5 z-50 p-0.5 bg-base-100 text-xs">Type</div>
<select name="entity_type" class="nb-select w-full">
<option disabled>You must select a type</option>
{% for et in entity_types %}
<option value="{{ et }}" {% if entity.entity_type==et %}selected{% endif %}>{{ et }}</option>
@@ -29,15 +29,13 @@ hx-swap="outerHTML"
<input type="text" name="id" value="{{ entity.id }}" class="hidden">
<div class="form-control">
<label class="floating-label">
<span class="label-text">Description</span>
<textarea name="description" class="w-full textarea textarea-bordered h-32">{{ entity.description }}</textarea>
<label class="w-full">
<div class="text-xs uppercase tracking-wide opacity-70 mb-1">Description</div>
<textarea name="description" class="nb-input w-full h-32">{{ entity.description }}</textarea>
</label>
</div>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
</button>
{% endblock %}
<button type="submit" class="nb-btn nb-cta">Save Changes</button>
{% endblock %}

View File

@@ -1,4 +1,4 @@
<div class="grid md:grid-cols-2 2xl:grid-cols-3 gap-4" id="entity-list">
<div class="grid md:grid-cols-2 2xl:grid-cols-3 gap-4 mt-6" id="entity-list">
{% for entity in entities %}
<div class="card min-w-72 bg-base-100 shadow">
<div class="card-body">
@@ -22,4 +22,4 @@
</div>
</div>
{% endfor %}
</div>
</div>

View File

@@ -1,12 +1,11 @@
<div id="relationship_table_section"
class="overflow-x-auto shadow rounded-box border border-base-content/5 bg-base-100 mb-10">
<table class="table">
<div id="relationship_table_section" class="overflow-x-auto nb-card mb-10">
<table class="nb-table">
<thead>
<tr>
<th>Origin</th>
<th>Target</th>
<th>Type</th>
<th>Actions</th>
<th class="text-left">Origin</th>
<th class="text-left">Target</th>
<th class="text-left">Type</th>
<th class="text-left">Actions</th>
</tr>
</thead>
<tbody>
@@ -31,9 +30,9 @@
{{ relationship.out }}
{% endfor %}
</td>
<td>{{ relationship.metadata.relationship_type }}</td>
<td class="uppercase tracking-wide text-xs">{{ relationship.metadata.relationship_type }}</td>
<td>
<button class="btn btn-sm btn-outline" hx-delete="/knowledge-relationship/{{ relationship.id }}"
<button class="nb-btn btn-xs" hx-delete="/knowledge-relationship/{{ relationship.id }}"
hx-target="#relationship_table_section" hx-swap="outerHTML">
{% include "icons/delete_icon.html" %}
</button>
@@ -43,7 +42,7 @@
<!-- New linking row -->
<tr id="new_relationship">
<td>
<select name="in_" class="select select-bordered w-full new_relationship_input">
<select name="in_" class="nb-select w-full new_relationship_input">
<option disabled selected>Select Origin</option>
{% for entity in entities %}
<option value="{{ entity.id }}">
@@ -53,7 +52,7 @@
</select>
</td>
<td>
<select name="out" class="select select-bordered w-full new_relationship_input">
<select name="out" class="nb-select w-full new_relationship_input">
<option disabled selected>Select Target</option>
{% for entity in entities %}
<option value="{{ entity.id }}">{{ entity.name }}</option>
@@ -62,12 +61,11 @@
</td>
<td>
<input id="relationship_type_input" name="relationship_type" type="text" placeholder="RelatedTo"
class="input input-bordered w-full new_relationship_input" />
class="nb-input w-full new_relationship_input" />
</td>
<td>
<button id="save_relationship_button" type="button" class="btn btn-primary btn-sm"
hx-post="/knowledge-relationship" hx-target="#relationship_table_section" hx-swap="outerHTML"
hx-include=".new_relationship_input">
<button id="save_relationship_button" type="button" class="nb-btn btn-sm" hx-post="/knowledge-relationship"
hx-target="#relationship_table_section" hx-swap="outerHTML" hx-include=".new_relationship_input">
Save
</button>
</td>

View File

@@ -1,21 +1,18 @@
<dialog id="body_modal" class="modal">
<div class="modal-box {% block modal_class %}{% endblock %} ">
<div class="modal-box rounded-none border-2 border-neutral bg-base-100 shadow-[8px_8px_0_0_#000] {% block modal_class %}{% endblock %}">
<form id="modal_form" {% block form_attributes %}{% endblock %}>
<div class="flex flex-col flex-1 space-y-4">
{% block modal_content %} <!-- Form fields go here in child templates -->
{% endblock %}
<div class="flex flex-col flex-1 gap-4">
{% block modal_content %}{% endblock %}
</div>
<div class="modal-action">
<div class="u-hairline mt-4 pt-3 flex justify-end gap-2">
<!-- Close button (always visible) -->
<button type="button" class="btn" onclick="document.getElementById('body_modal').close()">
<button type="button" class="nb-btn" onclick="document.getElementById('body_modal').close()">
Close
</button>
<!-- Primary actions block -->
{% block primary_actions %}
<!-- Submit/Save buttons go here in child templates -->
{% endblock %}
{% block primary_actions %}{% endblock %}
</div>
</form>
</div>
@@ -38,4 +35,4 @@
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</dialog>

View File

@@ -1,4 +1,4 @@
<nav class="bg-base-200 sticky top-0 z-10">
<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">
{% include "searchbar.html" %}
@@ -12,4 +12,4 @@
</ul>
</div>
</div>
</nav>
</nav>

View File

@@ -5,7 +5,13 @@
{% 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">
<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,8 +1,8 @@
{% if search_result is defined and search_result %}
<ul class="list shadow">
<ul class="nb-card p-0">
{% for result in search_result %}
<li class="list-row hover:bg-base-200/50 p-4">
<div class="w-10 h-10 flex-shrink-0 mr-4 self-start mt-1">
<li class="p-4 u-hairline hover:bg-base-200/40 flex gap-3">
<div class="w-10 h-10 flex-shrink-0 self-start mt-1 grid place-items-center border-2 border-neutral bg-base-100 shadow-[4px_4px_0_0_#000]">
{% if result.url_info and result.url_info.url %}
<div class="tooltip tooltip-right" data-tip="Web Link">
{% include "icons/link_icon.html" %}
@@ -17,10 +17,10 @@
</div>
{% endif %}
</div>
<div class="flex-grow min-w-0">
<h3 class="text-lg font-semibold mb-1">
<a hx-get="/content/{{ result.id }}/read" hx-target="#modal" hx-swap="innerHTML"
class="link link-hover link-primary">
<div class="flex-1 min-w-0">
<h3 class="text-lg font-extrabold mb-1 leading-snug">
<a hx-get="/content/{{ result.id }}/read" hx-target="#modal" hx-swap="innerHTML" class="nb-link">
{% set title_text = result.highlighted_url_title
| default(result.url_info.title if result.url_info else none, true)
| default(result.highlighted_file_name, true)
@@ -30,8 +30,7 @@
</a>
</h3>
<div class="markdown-content prose prose-sm text-sm text-base-content/80 mb-3 overflow-hidden line-clamp-6"
data-content="{{result.highlighted_text | escape}}">
<div class="markdown-content prose-tufte-compact text-base-content/80 mb-3 overflow-hidden line-clamp-6" data-content="{{result.highlighted_text | escape}}">
{% if result.highlighted_text %}
{{ result.highlighted_text | escape }}
{% elif result.text %}
@@ -41,43 +40,46 @@
{% endif %}
</div>
<div class="text-xs text-base-content/70 flex flex-wrap gap-x-4 gap-y-1 items-center">
<span class="inline-flex items-center"><strong class="font-medium mr-1">Category:</strong>
<span class="badge badge-soft badge-secondary badge-sm">{{ result.highlighted_category |
default(result.category, true) |
safe }}</span>
<div class="text-xs flex flex-wrap gap-x-4 gap-y-2 items-center">
<span class="inline-flex items-center">
<span class="uppercase tracking-wide opacity-60 mr-2">Category</span>
<span class="nb-badge">{{ result.highlighted_category | default(result.category, true) | safe }}</span>
</span>
{% if result.highlighted_context or result.context %}
<span class="inline-flex items-center"><strong class="font-medium mr-1">Context:</strong>
<span class="badge badge-sm badge-outline">{{ result.highlighted_context | default(result.context, true) |
safe }}</span>
<span class="inline-flex items-center min-w-0">
<span class="uppercase tracking-wide opacity-60 mr-2">Context</span>
<span class="nb-badge">{{ result.highlighted_context | default(result.context, true) | safe }}</span>
</span>
{% endif %}
{% if result.url_info and result.url_info.url %}
<span class="inline-flex items-center min-w-0"><strong class="font-medium mr-1">Source:</strong>
<a href="{{ result.url_info.url }}" target="_blank" class="link link-hover link-xs truncate"
title="{{ result.url_info.url }}">
<span class="inline-flex items-center min-w-0">
<span class="uppercase tracking-wide opacity-60 mr-2">Source</span>
<a href="{{ result.url_info.url }}" target="_blank" class="nb-link truncate" title="{{ result.url_info.url }}">
{{ result.highlighted_url | default(result.url_info.url ) | safe }}
</a>
</span>
{% endif %}
<span class="badge badge-ghost badge-sm">Score: {{ result.score }}</span>
<span class="inline-flex items-center">
<span class="uppercase tracking-wide opacity-60 mr-2">Score</span>
<span class="nb-badge">{{ result.score }}</span>
</span>
</div>
</div>
</li>
{% endfor %}
</ul>
</ul>
{% elif query_param is defined and query_param | trim != "" %}
<div class="p-4 text-center text-base-content/70">
<p class="text-xl font-semibold mb-2">No results found for "<strong>{{ query_param | escape }}</strong>".</p>
<p class="text-sm">Try using different keywords or checking for typos.</p>
</div>
<div class="nb-panel p-5 text-center">
<p class="text-xl font-extrabold mb-2">No results for “{{ query_param | escape }}.</p>
<p class="text-sm opacity-70">Try different keywords or check for typos.</p>
</div>
{% else %}
<div class="p-4 text-center text-base-content/70">
<p class="text-lg font-medium">Enter a term above to search your knowledge base.</p>
<p class="text-sm">Results will appear here.</p>
</div>
{% endif %}
<div class="nb-panel p-5 text-center">
<p class="text-lg font-semibold">Enter a term above to search your knowledge base.</p>
<p class="text-sm opacity-70">Results will appear here.</p>
</div>
{% endif %}

View File

@@ -1,8 +1,14 @@
<div class="flex items-center gap-2 min-w-[90px]">
<form class="w-full" hx-boost="true" method="get" action="/search"
<div class="flex items-center gap-2 min-w-[90px] w-full">
<form class="w-full relative" hx-boost="true" method="get" action="/search"
hx-trigger="keyup changed delay:500ms from:#search-input, search from:#search-input" hx-push-url="true">
<input id="search-input" type="search" placeholder="Search for anything..."
class="input input-sm input-bordered input-primary w-full" name="query" autocomplete="off"
value="{{ query_param | default('', true) }}" />
<input id="search-input" type="search" aria-label="Search" class=" nb-input w-full pl-9 ml-2" name="query"
autocomplete="off" value="{{ query_param | default('', true) }}" />
<button type="submit"
class="absolute right-1 top-1/2 -translate-y-1/2 nb-btn btn-xs px-3 h-7 bg-base-100 hover:bg-base-200">
Search
</button>
<span class="hidden md:inline absolute right-24 top-1/2 -translate-y-1/2 text-xs opacity-60">
press <kbd class="kbd kbd-xs">Enter</kbd>
</span>
</form>
</div>

View File

@@ -15,11 +15,12 @@
<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 bg-base-200 text-base-content flex flex-col">
<!-- <a class="px-2 mt-4 text-center text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a> -->
<ul class="menu p-0 w-72 h-full nb-canvas text-base-content flex flex-col border-r-2 border-neutral">
<!-- <a class="px-4 py-4 text-2xl font-extrabold tracking-tight text-primary border-b-2 border-neutral bg-base-100 nb-shadow" -->
<!-- href="/" hx-boost="true">Minne</a> -->
<!-- === TOP FIXED SECTION === -->
<div class="px-2 mt-14">
<div class="px-2 mt-4 space-y-3">
{% for url, name, label in [
("/", "home", "Dashboard"),
("/knowledge", "book", "Knowledge"),
@@ -28,22 +29,22 @@
("/search", "search", "Search")
] %}
<li>
<a hx-boost="true" href="{{ url }}" class="flex items-center gap-3">
<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>{{ label }}</span>
<span class="uppercase tracking-wide">{{ label }}</span>
</a>
</li>
{% endfor %}
<li>
<button class="btn btn-primary btn-outline 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
<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="divider "></div>
<div class="u-hairline mt-4"></div>
</div>
<!-- === MIDDLE SCROLLABLE SECTION === -->
<span class="menu-title pb-4 ">Recent Chats</span>
<span class="px-4 py-2 font-semibold tracking-wide">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 %}
@@ -51,12 +52,12 @@
{% 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">
<input type="text" name="title" value="{{ conversation.title }}" class="input input-sm flex-grow" />
<div class="flex gap-0.5">
<button type="submit" class="btn btn-ghost btn-xs">{% include "icons/check_icon.html" %}</button>
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">
class="btn btn-ghost btn-xs !p-0">
{% include "icons/x_icon.html" %}
</button>
</div>
@@ -86,29 +87,30 @@
</div>
<!-- === BOTTOM FIXED SECTION === -->
<div class="px-2 pb-4">
<div class="divider "></div>
<div class="px-2 pb-4 space-y-3">
<li>
<a hx-boost="true" href="/account" class="flex btn btn-ghost justify-start items-center gap-3">
<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>Account</span>
<span class="uppercase tracking-wide">Account</span>
</a>
</li>
{% if user.admin %}
<li>
<a hx-boost="true" href="/admin" class="flex btn btn-ghost justify-start items-center gap-3">
<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>Admin</span>
<span class="uppercase tracking-wide">Admin</span>
</a>
</li>
{% endif %}
<li>
<a hx-boost="true" href="/signout"
class="btn btn-error btn-outline w-full flex items-center gap-3 justify-start !mt-2">
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>Logout</span>
<span class="uppercase tracking-wide">Logout</span>
</a>
</li>
</div>
</ul>
</div>
</div>