refactor: better separation of dependencies to crates

node stuff to html crate only
This commit is contained in:
Per Stark
2025-04-04 12:50:38 +02:00
parent 54a67478cf
commit 69d23abd83
160 changed files with 231 additions and 337 deletions

View File

@@ -0,0 +1,38 @@
{% extends "modal_base.html" %}
{% block form_attributes %}
hx-patch="/update-ingestion-prompt"
hx-target="#system_prompt_section"
hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold mb-4">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">{{
settings.ingestion_system_prompt }}</textarea>
<p class="text-xs text-gray-500 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">
Reset to Default
</button>
<textarea id="default_prompt_content" style="display:none;">{{ default_ingestion_prompt }}</textarea>
<script>
document.getElementById('reset_prompt_button').addEventListener('click', function () {
const defaultContent = document.getElementById('default_prompt_content').value;
document.querySelector('textarea[name=ingestion_system_prompt]').value = defaultContent;
});
</script>
<button type="submit" class="btn btn-primary">
<span class="htmx-indicator hidden">
<span class="loading loading-spinner loading-xs mr-2"></span>
</span>
Save Changes
</button>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends "modal_base.html" %}
{% block form_attributes %}
hx-patch="/update-query-prompt"
hx-target="#system_prompt_section"
hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold mb-4">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">{{
settings.query_system_prompt }}</textarea>
<p class="text-xs text-gray-500 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">
Reset to Default
</button>
<textarea id="default_prompt_content" style="display:none;">{{ default_query_prompt }}</textarea>
<script>
document.getElementById('reset_prompt_button').addEventListener('click', function () {
const defaultContent = document.getElementById('default_prompt_content').value;
document.querySelector('textarea[name=query_system_prompt]').value = defaultContent;
});
</script>
<button type="submit" class="btn btn-primary">
<span class="htmx-indicator hidden">
<span class="loading loading-spinner loading-xs mr-2"></span>
</span>
Save Changes
</button>
{% endblock %}

View File

@@ -0,0 +1,119 @@
{% extends "body_base.html" %}
{% 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-3xl 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="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>
<script>
// Using a single toast element avoids creating many timeouts when clicking repeatedly.
let current_toast = null;
let toast_timeout = null;
function show_toast(message) {
if (current_toast) {
// Update message and reset timeout if a toast is already displayed.
current_toast.querySelector('span').textContent = message;
clearTimeout(toast_timeout);
} else {
current_toast = document.createElement('div');
current_toast.className = 'toast';
current_toast.innerHTML = `<div class="alert alert-success">
<div>
<span>${message}</span>
</div>
</div>`;
document.body.appendChild(current_toast);
}
toast_timeout = setTimeout(() => {
if (current_toast) {
current_toast.remove();
current_toast = null;
}
}, 3000);
}
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!');
}).catch(() => {
show_toast('Copy failed');
});
} else {
show_toast('Copy not supported');
}
}
</script>
<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>
<div class="form-control mt-4 hidden">
<button hx-post="/verify-email" class="btn btn-secondary w-full">
Verify Email
</button>
</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 %}

View File

@@ -0,0 +1,99 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="container flex-grow flex flex-col mx-auto mt-4 space-y-6">
<h1 class="text-3xl font-bold mb-2">Admin Dashboard</h1>
<div class="stats stats-vertical lg: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>
<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 sm: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>
<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>
</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">
<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">
<option value="gpt-4o-mini" {% if settings.query_model=="gpt-4o-mini" %}selected{% endif %}>GPT-4o Mini
</option>
<option value="gpt-4o" {% if settings.query_model=="gpt-4o" %}selected{% endif %}>GPT-4o</option>
<option value="gpt-3.5-turbo" {% if settings.query_model=="gpt-3.5-turbo" %}selected{% endif %}>GPT-3.5
Turbo</option>
</select>
<p class="text-xs text-gray-500 mt-1">Model used for answering user queries</p>
</div>
<div class="form-control my-4">
<label class="label">
<span class="label-text">Processing Model</span>
</label>
<select name="processing_model" class="select select-bordered w-full">
<option value="gpt-4o-mini" {% if settings.processing_model=="gpt-4o-mini" %}selected{% endif %}>GPT-4o Mini
</option>
<option value="gpt-4o" {% if settings.processing_model=="gpt-4o" %}selected{% endif %}>GPT-4o</option>
<option value="gpt-3.5-turbo" {% if settings.processing_model=="gpt-3.5-turbo" %}selected{% endif %}>GPT-3.5
Turbo</option>
</select>
<p class="text-xs text-gray-500 mt-1">Model used for content processing and ingestion</p>
</div>
<button type="submit" class="btn btn-primary btn-sm">Save Model Settings</button>
</form>
{% 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 %} />
</form>
{% endblock %}
Enable Registrations
</label>
<div id="registration-status" class="text-sm mt-2"></div>
</fieldset>
</div>
</main>
{% endblock %}

View File

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

View File

@@ -0,0 +1,6 @@
{% extends "head_base.html" %}
{% block body %}
<div class="min-h-[100dvh] flex">
{% include "auth/signin_form.html" %}
</div>
{% endblock %}

View File

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

View File

@@ -0,0 +1,61 @@
{% extends "head_base.html" %}
{% block body %}
<style>
form.htmx-request {
opacity: 0.5;
}
</style>
<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="">
<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>
</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>
// Detect timezone and set hidden input
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.getElementById("timezone").value = timezone;
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "head_base.html" %}
{% block body %}
<body>
<div class="min-h-screen flex flex-col">
<!-- Navbar -->
{% include "navigation_bar.html" %}
<!-- Main Content -->
{% block main %}{% endblock %}
<div id="modal"></div>
</div>
</body>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends 'body_base.html' %}
{% block main %}
<div class="drawer xl:drawer-open h-[calc(100vh-65px)] overflow-auto">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<!-- Drawer Content -->
<div class="drawer-content flex justify-center ">
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 max-w-3xl w-full absolute left-0 right-0 mx-auto">
<div class="relative w-full">
{% include "chat/history.html" %}
{% include "chat/new_message_form.html" %}
</div>
</main>
</div>
<!-- Drawer Sidebar -->
{% include "chat/drawer.html" %}
</div>
<style>
/* Custom styles to override DaisyUI defaults */
.drawer-content {
width: 100%;
padding: 0;
}
.drawer-side {
z-index: 20;
/* Ensure drawer is above content */
}
@media (min-width: 1280px) {
/* xl breakpoint */
.drawer-open .drawer-content {
margin-left: 0;
/* Prevent content shift */
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,15 @@
<div class="drawer-side z-50 max-h-[calc(100vh-65px)]">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content w-72">
<!-- Sidebar content here -->
<li class="mt-4 cursor-pointer "><a href="/chat" hx-boost="true" class="flex justify-between">Create new
chat<span>{% include
"icons/edit_icon.html" %}
</span></a></li>
<div class="divider"></div>
{% for conversation in conversation_archive %}
<li><a href="/chat/{{conversation.id}}" hx-boost="true">{{conversation.title}} - {{conversation.created_at}}</a>
</li>
{% endfor %}
</ul>
</div>

View File

@@ -0,0 +1,54 @@
<div id="chat_container" class="pl-3 overflow-y-auto h-[calc(100vh-175px)] hide-scrollbar">
{% for message in history %}
{% if message.role == "AI" %}
<div class="chat chat-start">
<div>
<div class="chat-bubble">
{{ message.content }}
</div>
{% if message.references %}
{% include "chat/reference_list.html" %}
{% endif %}
</div>
</div>
{% else %}
<div class="chat chat-end">
<div class="chat-bubble">
{{ message.content }}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<script>
document.body.addEventListener('htmx:afterSwap', function (evt) {
const chatContainer = document.getElementById('chat_container');
if (chatContainer) {
setTimeout(() => {
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 0);
}
});
window.addEventListener('load', function () {
const chatContainer = document.getElementById('chat_container');
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
});
</script>
<style>
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
/* Chrome, Safari and Opera */
}
</style>

View File

@@ -0,0 +1,29 @@
{% 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>
<button type="submit" class="absolute p-2 cursor-pointer right-0.5 btn-ghost btn-sm top-1">
{% include "icons/send_icon.html" %}
</button>
<label for="my-drawer-2" class="absolute cursor-pointer top-9 right-0.5 p-2 drawer-button xl:hidden z-20 ">
{% include "icons/hamburger_icon.html" %}
</label>
</form>
<script>
document.getElementById('chat-input').addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
htmx.trigger('#chat-form', 'submit');
}
});
// Clear textarea after successful submission
document.getElementById('chat-form').addEventListener('htmx:afterRequest', function (e) {
if (e.detail.successful) { // Check if the request was successful
document.getElementById('chat-input').value = ''; // Clear the textarea
}
});
</script>

View File

@@ -0,0 +1,29 @@
<div class="fixed w-full mx-auto max-w-3xl p-0 pb-0 sm:pb-4 left-0 right-0 bottom-0 bg-base-100 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"
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>
<label for="my-drawer-2" class="absolute cursor-pointer top-9 right-0.5 p-2 drawer-button xl:hidden z-20 ">
{% include "icons/hamburger_icon.html" %}
</label>
</form>
</div>
<script>
document.getElementById('chat-input').addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
htmx.trigger('#chat-form', 'submit');
}
});
// Clear textarea after successful submission
document.getElementById('chat-form').addEventListener('htmx:afterRequest', function (e) {
if (e.detail.successful) { // Check if the request was successful
document.getElementById('chat-input').value = ''; // Clear the textarea
}
});
</script>

View File

@@ -0,0 +1,90 @@
<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">
References
{% include "icons/chevron_icon.html" %}
</button>
<div id="references-content-{{message.id}}" class="hidden max-w-full mt-1">
<div class="flex flex-wrap gap-1">
{% 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">
{{reference}}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
<script>
document.getElementById('references-toggle-{{message.id}}').addEventListener('click', function () {
const content = document.getElementById('references-content-{{message.id}}');
const icon = document.getElementById('toggle-icon');
content.classList.toggle('hidden');
icon.classList.toggle('rotate-180');
});
// Initialize portal tooltips
document.querySelectorAll('.reference-badge-container').forEach(container => {
const reference = container.dataset.reference;
const messageId = container.dataset.messageId;
const index = container.dataset.index;
let tooltipId = `tooltip-${messageId}-${index}`;
let tooltipContent = null;
let tooltipTimeout;
// Create tooltip element (initially hidden)
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.innerHTML = '<div class="animate-pulse">Loading...</div>';
document.body.appendChild(tooltip);
return tooltip;
}
container.addEventListener('mouseenter', function () {
// Clear any existing timeout
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// Get or create tooltip
let tooltip = document.getElementById(tooltipId);
if (!tooltip) tooltip = createTooltip();
// Position tooltip
const rect = container.getBoundingClientRect();
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
tooltip.style.left = `${rect.left + window.scrollX}px`;
// Adjust position if it would overflow viewport
const tooltipRect = tooltip.getBoundingClientRect();
if (rect.left + tooltipRect.width > window.innerWidth - 20) {
tooltip.style.left = `${window.innerWidth - tooltipRect.width - 20 + window.scrollX}px`;
}
// Show tooltip
tooltip.classList.remove('hidden');
// Load content if needed
if (!tooltipContent) {
fetch(`/chat/reference/${encodeURIComponent(reference)}`)
.then(response => response.text())
.then(html => {
tooltipContent = html;
if (document.getElementById(tooltipId)) {
document.getElementById(tooltipId).innerHTML = html;
}
});
}
});
container.addEventListener('mouseleave', function () {
tooltipTimeout = setTimeout(() => {
const tooltip = document.getElementById(tooltipId);
if (tooltip) tooltip.classList.add('hidden');
}, 200);
});
});
</script>

View File

@@ -0,0 +1,3 @@
<div>{{entity.name}}</div>
<div>{{entity.description}}</div>
<div>{{entity.updated_at|datetimeformat(format="short", tz=user.timezone)}} </div>

View File

@@ -0,0 +1,27 @@
<div class="chat chat-end">
<div class="chat-bubble">
{{user_message.content}}
</div>
</div>
<div class="chat chat-start">
<div hx-ext="sse" sse-connect="/chat/response-stream?message_id={{user_message.id}}" sse-close="close_stream"
hx-swap="beforeend">
<div class="chat-bubble" sse-swap="chat_message">
<span class="loading loading-dots loading-sm loading-id-{{user_message.id}}"></span>
</div>
<div class="chat-footer opacity-50 max-w-[90%] flex-wrap" sse-swap="references">
</div>
</div>
</div>
<script>
document.body.addEventListener('htmx:sseBeforeMessage', (e) => {
const targetElement = e.detail.elt;
const loadingSpinner = targetElement.querySelector('.loading-id-{{user_message.id}}');
// Hiding the loading spinner before data is swapped in
if (loadingSpinner) {
loadingSpinner.style.display = 'none';
}
}
)
</script>

View File

@@ -0,0 +1,12 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10">
<div class="container">
<h2 class="text-2xl font-bold mb-2">Text Contents</h2>
{% include "content/content_list.html" %}
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,46 @@
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4" id="text_content_cards">
{% for text_content in text_contents %}
<div class="card min-w-72 bg-base-100 shadow">
<div class="card-body">
<div class="flex items-center space-x-2">
<div class="flex-shrink-0">
{% if text_content.url %}
{% include "icons/globe_icon.html" %}
{% elif text_content.file_info %}
{% include "icons/document_icon.html" %}
{% else %}
{% include "icons/chat_icon.html" %}
{% endif %}
</div>
<h2 class="card-title truncate">
{% if text_content.url %}
<a href="{{ text_content.url}}">{{text_content.url}}</a>
{% elif text_content.file_info %}
{{text_content.file_info.file_name}}
{% else %}
{{text_content.text}}
{% endif %}
</h2>
</div>
<div class="flex items-center">
<p class="text-xs opacity-60">
{{ text_content.created_at | datetimeformat(format="short", tz=user.timezone) }}
</p>
<div class="flex gap-2">
<button hx-get="/content/{{ text_content.id }}" hx-target="#modal" hx-swap="innerHTML"
class="btn btn-square btn-ghost btn-sm">
{% 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">
{% include "icons/delete_icon.html" %}
</button>
</div>
</div>
<p class="mt-2">
{{ text_content.instructions }}
</p>
</div>
</div>
{% endfor %}
</div>

View File

@@ -0,0 +1,37 @@
{% extends "modal_base.html" %}
{% block form_attributes %}
hx-patch="/content/{{text_content.id}}"
hx-target="#text_content_cards"
hx-swap="outerHTML"
{% 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">Instructions</span>
<input type="text" name="instructions" value="{{ text_content.instructions}}" 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">
<label class="floating-label">
<span class="label-text">Text</span>
<textarea name="text" class="textarea textarea-bordered h-32 w-full">{{ text_content.text}}</textarea>
</label>
</div>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
</button>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% 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">
<h1 class="text-2xl font-bold text-error">
{{ status_code }}
</h1>
<p class="text-2xl my-4">{{ error }}</p>
<p class="text-base-content/60">{{ description }}</p>
<a href="/" class="btn btn-primary mt-8">Go Home</a>
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}Minne{% endblock %}</title>
<!-- <meta http-equiv=" refresh" content="4"> -->
<!-- Preload critical assets -->
<link rel="preload" href="/assets/htmx.min.js" as="script">
<link rel="preload" href="/assets/htmx-ext-sse.js" as="script">
<link rel="preload" href="/assets/style.css" as="style">
<!-- Core styles -->
<link rel="stylesheet" href="/assets/style.css">
<!-- Scripts -->
<script src="/assets/htmx.min.js" defer></script>
<script src="/assets/htmx-ext-sse.js" defer></script>
<script src="/assets/theme-toggle.js" defer></script>
<!-- Icons -->
<link rel="icon" href="/assets/icon/favicon.ico">
<link rel="apple-touch-icon" href="/assets/icon/apple-touch-icon.png" media="(device-width: 320px)">
<!-- PWA -->
<link rel="manifest" href="/assets/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
{% block head %}{% endblock %}
</head>
{% block body %}{% endblock %}
<script>
(function wait_for_htmx() {
if (window.htmx) {
htmx.config.globalViewTransitions = true;
} else {
setTimeout(wait_for_htmx, 50);
}
})();
</script>
</html>

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@@ -0,0 +1,6 @@
<svg class="w-3 h-3 ml-1 transition-transform" id="toggle-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1,8 @@
{% extends "body_base.html" %}
{% block main %}
{% if user %}
{% include 'index/signed_in/base.html' %}
{% else %}
{% include 'auth/signin_form.html' %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% block active_jobs_section %}
<ul id="active_jobs_section" class="list">
<div class="flex justify-center items-center gap-4">
<li class="py-4 text-center font-bold tracking-wide">Active Jobs</li>
<button class="cursor-pointer scale-75" hx-get="/active-jobs" hx-target="#active_jobs_section" hx-swap="outerHTML">
{% 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/globe_icon.html" %}
{% elif item.content.File %}
{% include "icons/document_icon.html" %}
{% else %}
{% include "icons/chat_icon.html" %}
{% endif %}
</div>
<div>
<div class="[&:before]:content-['Status:_'] [&:before]:opacity-60">
{% if item.status.InProgress %}
In Progress, attempt {{item.status.InProgress.attempts}}
{% else %}
{{item.status}}
{% endif %}
</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 class="btn disabled btn-square btn-ghost btn-sm"> -->
<!-- {% include "icons/edit_icon.html" %} -->
<!-- </button> -->
<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>
{% endblock %}

View File

@@ -0,0 +1,14 @@
<div class="flex justify-center grow mt-2 sm:mt-4 gap-6">
<div class="container">
{% include 'index/signed_in/searchbar.html' %}
{% include "index/signed_in/quick_actions.html" %}
<div class="grid grid-cols-1 md:grid-cols-2 shadow my-10">
{% include "index/signed_in/active_jobs.html" %}
{% include "index/signed_in/recent_content.html" %}
</div>
</div>
</div>

View File

@@ -0,0 +1,57 @@
{% extends "modal_base.html" %}
{% block form_attributes %}
hx-post="/ingress-form"
enctype="multipart/form-data"
hx-target="#active_jobs_section"
hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold">Add new content</h3>
<div class="form-control">
<label class="floating-label">
<span>Instructions</span>
<textarea name="instructions" class="textarea w-full validator"
placeholder="Enter instructions for the AI here, help it understand what its seeing or how it should relate to the database"
required>{{ instructions }}</textarea>
<div class="validator-hint hidden">Instructions are required</div>
</label>
</div>
<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 ingress, it can be an URL or a text snippet">{{ content }}</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 />
<datalist id="category-list">
{% for category in user_categories %}
<option value="{{ category }}" />
{% endfor %}
</datalist>
<div class="validator-hint hidden">Category is required</div>
</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>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
</button>
{% endblock %}

View File

@@ -0,0 +1,7 @@
<div class="flex gap-4 flex-col sm:flex-row">
<a class="btn btn-secondary" href="/knowledge" hx-boost="true">View Knowledge</a>
<a class="btn btn-accent" href="/content" hx-boost="true">View Content</a>
<a class="btn btn-accent" href="/chat" hx-boost="true">Chat</a>
<button class="btn btn-primary" hx-get="/ingress-form" hx-target="#modal" hx-swap="innerHTML">Add
Content</button>
</div>

View File

@@ -0,0 +1,42 @@
{% block latest_content_section %}
<ul id="latest_content_section" class="list">
<li class="py-4 text-center font-bold tracking-wide">Recently added content</li>
{% for item in latest_text_contents %}
<li class="list-row">
<div class="bg-accent rounded-box size-10 flex justify-center items-center text-accent-content">
{% if item.url %}
{% include "icons/globe_icon.html" %}
{% elif item.file_info %}
{% include "icons/document_icon.html" %}
{% else %}
{% include "icons/chat_icon.html" %}
{% endif %}
</div>
<div>
<div class="truncate max-w-[160px]">
{% if item.url %}
{{item.url}}
{% elif item.file_info%}
{{item.file_info.file_name}}
{% else %}
{{item.text}}
{% endif %}
</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-['Instructions:_'] [&:before]:opacity-60">
{{item.instructions}}
</p>
<button class="btn btn-disabled btn-square btn-ghost btn-sm">
{% include "icons/edit_icon.html" %}
</button>
<button hx-delete="/text-content/{{item.id}}" hx-target="#latest_content_section" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/delete_icon.html" %}
</button>
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -0,0 +1,32 @@
<div class="mx-auto mb-6">
<div class="card bg-base-200 shadow">
<div class="card-body">
<h2 class="card-title">Result</h2>
<div class="prose !max-w-none">
{{ answer_content | safe}}
</div>
{% if answer_references %}
<div class="mt-4">
<h2 class="card-title mb-2">References</h2>
<div class="flex flex-wrap gap-2 max-w-full">
{% for ref in answer_references %}
<div class="tooltip" data-tip="More info about {{ ref }}">
<button class="badge truncate badge-outline cursor-pointer text-gray-500 hover:text-gray-700">
{{ ref }}
</button>
</div>
{% endfor %}
</div>
</div>{% endif %}
<div class="mt-4">
<form hx-post="/initialized-chat" hx-target="body" hx-swap="outerHTML" method="POST"
class="flex items-center space-x-4">
<input type="hidden" name="user_query" value="{{ user_query }}">
<input type="hidden" name="llm_response" value="{{ answer_content }}">
<input type="hidden" name="references" value="{{ answer_references }}">
<button type="submit" class="btn btn-primary">Continue with chat</button>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<h2 class="font-bold mb-2">
Search your content
</h2>
<input type="text" placeholder="Search your knowledge base" class="input input-bordered w-full" name="query"
hx-get="/search" hx-target="#search-results" />
<div id="search-results" class="mt-4">
<!-- Results will be populated here by HTMX -->
</div>

View File

@@ -0,0 +1,19 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10">
<div class="container">
<h2 class="text-2xl font-bold mb-2">Entities</h2>
{% include "knowledge/entity_list.html" %}
<h2 class="text-2xl font-bold mb-2 mt-10">Relationships</h2>
{% include "knowledge/relationship_table.html" %}
<div class="rounded-box overflow-clip mt-10 shadow">
{{plot_html|safe}}
</div>
</div>
</main>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "modal_base.html" %}
{% block form_attributes %}
hx-patch="/knowledge-entity/{{entity.id}}"
hx-target="#entity-list"
hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold">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>
</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">
<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>
{% endfor %}
</select>
</div>
<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>
</div>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
</button>
{% endblock %}

View File

@@ -0,0 +1,25 @@
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4" id="entity-list">
{% for entity in entities %}
<div class="card min-w-72 bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title">{{entity.name}}
<span class="badge badge-xs badge-primary">{{entity.entity_type}}</span>
</h2>
<div class="flex justify-between items-center">
<p>{{entity.updated_at | datetimeformat(format="short", tz=user.timezeone)}}</p>
<div>
<button hx-get="/knowledge-entity/{{entity.id}}" hx-target="#modal" hx-swap="innerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/edit_icon.html" %}
</button>
<button hx-delete="/knowledge-entity/{{entity.id}}" hx-target="#entity-list" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/delete_icon.html" %}
</button>
</div>
</div>
<p>{{entity.description}}</p>
</div>
</div>
{% endfor %}
</div>

View File

@@ -0,0 +1,85 @@
<div id="relationship_table_section"
class="overflow-x-auto shadow rounded-box border border-base-content/5 bg-base-100">
<table class="table">
<thead>
<tr>
<th>Origin</th>
<th>Target</th>
<th>Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for relationship in relationships %}
<tr>
<!-- Origin column -->
<td>
{% for entity in entities if entity.id == relationship.in %}
<span> {{ entity.name }}
</span>
{% else %}
{{ relationship.in }}
{% endfor %}
</td>
<!-- Target column -->
<td>
{% for entity in entities if entity.id == relationship.out %}
<span>
{{ entity.name }}
</span>
{% else %}
{{ relationship.out }}
{% endfor %}
</td>
<td>{{ relationship.metadata.relationship_type }}</td>
<td>
<button class="btn btn-sm btn-outline" hx-delete="/knowledge-relationship/{{ relationship.id }}"
hx-target="#relationship_table_section" hx-swap="outerHTML">
{% include "icons/delete_icon.html" %}
</button>
</td>
</tr>
{% endfor %}
<!-- New linking row -->
<tr id="new_relationship">
<td>
<select name="in_" class="select select-bordered w-full new_relationship_input">
<option disabled selected>Select Origin</option>
{% for entity in entities %}
<option value="{{ entity.id }}">
{{ entity.name }}
</option>
{% endfor %}
</select>
</td>
<td>
<select name="out" class="select select-bordered w-full new_relationship_input">
<option disabled selected>Select Target</option>
{% for entity in entities %}
<option value="{{ entity.id }}">{{ entity.name }}</option>
{% endfor %}
</select>
</td>
<td>
<input id="relationship_type_input" name="relationship_type" type="text" placeholder="RelatedTo"
class="input input-bordered 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">
Save
</button>
</td>
</tr>
</tbody>
</table>
</div>
<script>
document.getElementById('relationship_type_input').addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault(); // Prevent form submission if within a form
document.getElementById('save_relationship_button').click();
}
});
</script>

View File

@@ -0,0 +1,38 @@
<dialog id="body_modal" class="modal">
<div class="modal-box">
<form id="modal_form" {% block form_attributes %}{% endblock %}>
<div class="flex flex-col space-y-4">
{% block modal_content %} <!-- Form fields go here in child templates -->
{% endblock %}
</div>
<div class="modal-action">
<!-- Close button (always visible) -->
<button type="button" class="btn" onclick="document.getElementById('body_modal').close()">
Close
</button>
<!-- Primary actions block -->
{% block primary_actions %}
<!-- Submit/Save buttons go here in child templates -->
{% endblock %}
</div>
</form>
</div>
<script>
// Auto-open modal when injected
document.getElementById('body_modal').showModal();
// Close modal on successful form submission
document.getElementById('modal_form').addEventListener('htmx:afterRequest', (evt) => {
if (evt.detail.successful) {
document.getElementById('body_modal').close();
}
});
// Clear modal content on close to prevent browser back from reopening it
document.getElementById('body_modal').addEventListener('close', (evt) => {
evt.target.innerHTML = '';
});
</script>
</dialog>

View File

@@ -0,0 +1,29 @@
<nav class="navbar bg-base-200 !p-0">
<div class="container flex mx-auto">
<div class="flex-1 flex items-center">
<a class="text-2xl p-2 text-primary font-bold" href="/" hx-boost="true">Minne</a>
</div>
<div>
<ul class="menu menu-horizontal px-1">
{% include "theme_toggle.html" %}
<li><a hx-boost="true" class="" href="/documentation">Docs</a></li>
{% if user %}
<li>
<details>
<summary>Account</summary>
<ul class="bg-base-100 rounded-t-none p-2 z-50">
<li><a hx-boost="true" href="/account">Account</a></li>
{% if user.admin %}
<li><a hx-boost="true" href="/admin">Admin</a></li>
{% endif %}
<li><a hx-boost="true" href="/signout">Sign out</a></li>
</ul>
</details>
</li>
{% else %}
<li><a hx-boost="true" class="" href="/signin">Sign in</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>

View File

@@ -0,0 +1,41 @@
<!-- Theme switch script -->
<script>
const initializeTheme = () => {
console.log("Initializing theme toggle...");
const themeToggle = document.querySelector('.theme-controller');
if (!themeToggle) {
console.log("Theme toggle not found.");
return;
}
// Detect system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Initialize theme from local storage or system preference
const savedTheme = localStorage.getItem('theme');
const initialTheme = savedTheme ? savedTheme : (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', initialTheme);
themeToggle.checked = initialTheme === 'dark';
// Update theme and local storage on toggle
themeToggle.addEventListener('change', () => {
const theme = themeToggle.checked ? 'dark' : 'light';
console.log("Theme switched to:", theme);
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
});
console.log("Theme toggle initialized.");
};
// Run the initialization after the DOM is fully loaded
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM fully loaded. Initializing theme toggle...");
initializeTheme();
});
// Reinitialize theme toggle after HTMX swaps
document.addEventListener('htmx:afterSwap', initializeTheme);
document.addEventListener('htmx:afterSettle', initializeTheme);
</script>

View File

@@ -0,0 +1,9 @@
<div class="border-">
<div class="chat chat-start">
<div class="chat-bubble">
{{result}}
<hr />
{{references}}
</div>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<label class="swap swap-rotate mr-1">
<!-- this hidden checkbox controls the state -->
<input type="checkbox" class="theme-controller" value="dark" />
<!-- sun icon -->
<svg width="20" height="20" class="swap-off h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg width="20" height="20" class="swap-on h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>