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

@@ -30,6 +30,7 @@ tower-http = { workspace = true }
chrono-tz = { workspace = true }
tower-serve-static = { workspace = true }
tokio-util = { workspace = true }
chrono = { workspace = true }
common = { path = "../common" }
composite-retrieval = { path = "../composite-retrieval" }

View File

@@ -1,29 +1,108 @@
@import 'tailwindcss' source(none);
@import 'tailwindcss';
@source './templates/**/*.html';
@plugin "daisyui" {
exclude: rootscrollbargutter;
logs: false;
themes: false;
include: [ "properties",
"scrollbar",
"rootscrolllock",
"rootcolor",
"svg",
"button",
"menu",
"navbar",
"drawer",
"modal",
"chat",
"card",
"loading",
"validator",
"fileinput",
"alert",
"swap"
];
}
@plugin "@tailwindcss/typography";
@config './tailwind.config.js';
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@view-transition {
navigation: auto;
}
@layer base {
:root {
--nb-shadow: 4px 4px 0 0 #000;
--nb-shadow-hover: 6px 6px 0 0 #000;
}
[data-theme="light"] {
color-scheme: light;
--color-base-100: oklch(98.42% 0.012 96.42);
--color-base-200: oklch(94.52% 0.0122 96.43);
--color-base-300: oklch(90.96% 0.0125 91.53);
--color-base-content: oklch(17.76% 0 89.88);
--color-primary: oklch(20.77% 0.0398 265.75);
--color-primary-content: oklch(100% 0 89.88);
--color-secondary: oklch(54.61% 0.2152 262.88);
--color-secondary-content: oklch(100% 0 89.88);
--color-accent: oklch(72% 0.19 80);
--color-accent-content: oklch(21% 0.035 80);
--color-neutral: oklch(17.76% 0 89.88);
--color-neutral-content: oklch(96.99% 0.0013 106.42);
--color-info: oklch(60.89% 0.1109 221.72);
--color-info-content: oklch(96.99% 0.0013 106.42);
--color-success: oklch(62.71% 0.1699 149.21);
--color-success-content: oklch(96.99% 0.0013 106.42);
--color-warning: oklch(79.52% 0.1617 86.05);
--color-warning-content: oklch(17.76% 0 89.88);
--color-error: oklch(57.71% 0.2152 27.33);
--color-error-content: oklch(96.99% 0.0013 106.42);
--radius-selector: 0rem;
--radius-field: 0rem;
--radius-box: 0rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 2px;
}
[data-theme="dark"] {
color-scheme: dark;
--color-base-100: oklch(22% 0.015 255);
--color-base-200: oklch(18% 0.014 253);
--color-base-300: oklch(14% 0.012 251);
--color-base-content: oklch(97.2% 0.02 255);
--color-primary: oklch(58% 0.233 277.12);
--color-primary-content: oklch(96% 0.018 272.31);
--color-secondary: oklch(65% 0.241 354.31);
--color-secondary-content: oklch(94% 0.028 342.26);
--color-accent: oklch(78% 0.22 80);
--color-accent-content: oklch(20% 0.035 80);
--color-neutral: oklch(26% 0.02 255);
--color-neutral-content: oklch(97% 0.03 255);
--color-info: oklch(74% 0.16 232.66);
--color-info-content: oklch(29% 0.066 243.16);
--color-success: oklch(76% 0.177 163.22);
--color-success-content: oklch(37% 0.077 168.94);
--color-warning: oklch(82% 0.189 84.43);
--color-warning-content: oklch(41% 0.112 45.9);
--color-error: oklch(71% 0.194 13.43);
--color-error-content: oklch(27% 0.105 12.09);
--radius-selector: 0rem;
--radius-field: 0rem;
--radius-box: 0rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 2px;
}
body {
@apply font-satoshi;
background-color: var(--color-base-100);
color: var(--color-base-content);
font-family: 'Satoshi', sans-serif;
-webkit-font-smoothing: antialiased;
@apply selection:bg-yellow-300/40 selection:text-neutral;
}
html {
@@ -37,6 +116,479 @@
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
.container {
padding-inline: 10px;
}
@media (min-width: 640px) {
.container {
padding-inline: 2rem;
}
}
@media (min-width: 1024px) {
.container {
padding-inline: 4rem;
}
}
@media (min-width: 1280px) {
.container {
padding-inline: 5rem;
}
}
@media (min-width: 1536px) {
.container {
padding-inline: 6rem;
}
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
.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;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
form.htmx-request {
opacity: 0.5;
}
}
/* Neobrutalist helpers influenced by Tufte principles */
@layer components {
/* Offset, hard-edge shadow; minimal ink with strong contrast */
.nb-shadow {
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
.nb-shadow-hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
.nb-card {
@apply bg-base-100 border-2 border-neutral p-4;
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
.nb-card:hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
.nb-panel {
@apply border-2 border-neutral;
background-color: var(--nb-panel-bg, var(--color-base-200));
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
.nb-panel:hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
.nb-panel-canvas {
--nb-panel-bg: var(--color-base-100);
}
.nb-canvas {
background-color: var(--color-base-100);
}
.nb-btn {
@apply btn rounded-none border-2 border-neutral text-base-content;
--btn-color: var(--color-base-100);
--btn-fg: var(--color-base-content);
--btn-noise: none;
background-image: none;
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
.nb-btn:hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
.nb-link {
@apply underline underline-offset-2 decoration-neutral hover:decoration-4;
}
.nb-stat {
@apply bg-base-100 border-2 border-neutral p-5 flex flex-col gap-1;
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
/* Hairline rules and quiet gridlines for Tufte feel */
.u-hairline {
@apply border-t border-neutral/20;
}
.prose-tufte {
@apply prose prose-neutral;
max-width: min(90ch, 100%);
line-height: 1.7;
}
.prose-tufte-compact {
@apply prose prose-neutral;
max-width: min(90ch, 100%);
font-size: 0.875rem;
line-height: 1.6;
}
/* Encourage a consistent card look app-wide */
.card {
@apply border-2 border-neutral rounded-none;
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
.card:hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
/* Input styling with good dark/light contrast */
.nb-input {
@apply rounded-none border-2 border-neutral bg-base-100 text-base-content placeholder:text-base-content/60 px-3 py-[0.5rem];
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms, border-color 150ms;
}
.nb-input:hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
.nb-input:focus {
outline: none;
box-shadow: var(--nb-shadow-hover);
}
/* Select styling parallels inputs */
.nb-select {
@apply rounded-none border-2 border-neutral bg-base-100 text-base-content px-3 py-[0.5rem];
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms, border-color 150ms;
}
.nb-select:hover {
transform: translate(-1px, -1px);
box-shadow: var(--nb-shadow-hover);
}
.nb-select:focus {
outline: none;
box-shadow: var(--nb-shadow-hover);
}
/* Compact variants */
.nb-input-sm {
@apply text-sm px-2 py-[0.25rem];
}
.nb-select-sm {
@apply text-sm px-2 py-[0.25rem];
}
.nb-cta {
--btn-color: var(--color-accent);
--btn-fg: var(--color-accent-content);
--btn-noise: none;
background-image: none;
background-color: var(--color-accent);
color: var(--color-accent-content);
}
.nb-cta:hover {
background-color: var(--color-accent);
color: var(--color-accent-content);
filter: saturate(1.1) brightness(1.05);
}
/* Badges */
.nb-badge {
@apply inline-flex items-center uppercase tracking-wide text-[10px] px-2 py-0.5 bg-base-100 border-2 border-neutral rounded-none;
box-shadow: 3px 3px 0 0 #000;
}
.nb-masonry {
column-count: 1;
column-gap: 1rem;
}
.nb-masonry>* {
break-inside: avoid;
display: block;
}
@media (min-width: 768px) {
.nb-masonry {
column-count: 2;
}
}
@media (min-width: 1536px) {
.nb-masonry {
column-count: 3;
}
}
/* Chat bubbles neobrutalist */
.chat .chat-bubble {
@apply rounded-none border-2 border-neutral bg-base-100 text-neutral;
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms;
}
/* Remove DaisyUI tail so our rectangle keeps clean borders/shadows */
.chat .chat-bubble::before,
.chat .chat-bubble::after {
display: none !important;
content: none !important;
}
.chat.chat-start .chat-bubble {
@apply bg-secondary text-secondary-content;
}
.chat.chat-end .chat-bubble {
@apply bg-base-100 text-neutral;
}
/* Tables */
.nb-table {
@apply w-full;
border-collapse: separate;
border-spacing: 0;
}
.nb-table thead th {
@apply uppercase tracking-wide text-xs border-b-2 border-neutral;
}
.nb-table th,
.nb-table td {
@apply p-3;
}
.nb-table tbody tr+tr td {
@apply border-t border-neutral/30;
}
.nb-table tbody tr:hover {
@apply bg-base-200/40;
}
.nb-table tbody tr:hover td:first-child {
box-shadow: inset 3px 0 0 0 #000;
}
.kg-overlay {
@apply absolute top-4 left-4 z-10 flex items-center gap-2;
}
.kg-search-input {
@apply pl-2;
height: 2rem;
min-width: 220px;
max-width: 320px;
}
.kg-legend {
@apply absolute bottom-2 left-2 z-10 flex flex-wrap gap-4;
}
.kg-legend-card {
@apply p-2;
}
.kg-legend-heading {
@apply mb-1 text-xs opacity-70;
}
.kg-legend-row {
@apply flex items-center gap-2 text-xs;
}
/* Checkboxes */
.nb-checkbox {
@apply appearance-none inline-block align-middle rounded-none border-2 border-neutral bg-base-100;
width: 1rem;
height: 1rem;
box-shadow: var(--nb-shadow);
transition: transform 150ms, box-shadow 150ms, border-color 150ms, background-color 150ms;
background-repeat: no-repeat;
background-position: center;
background-size: 80% 80%;
cursor: pointer;
}
.nb-checkbox:hover {
transform: translate(-1px, -1px);
box-shadow: 5px 5px 0 0 #000;
}
.nb-checkbox:focus-visible {
outline: 2px solid #000;
outline-offset: 2px;
}
.nb-checkbox:active {
transform: translate(0, 0);
box-shadow: 3px 3px 0 0 #000;
}
/* Tick mark in light mode (black) */
.nb-checkbox:checked {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
}
/* Tick mark in dark mode (white) */
[data-theme="dark"] .nb-checkbox:checked {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
}
/* Compact size */
.nb-checkbox-sm {
width: 0.875rem;
height: 0.875rem;
}
/* Placeholder style for smaller, quieter helper text */
.nb-input::placeholder {
font-size: 0.75rem;
letter-spacing: 0.02em;
opacity: 0.75;
}
.markdown-content {
line-height: 1.5;
word-wrap: break-word;
}
.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 table {
border-collapse: collapse;
margin: 0.75em 0;
width: 100%;
}
.markdown-content th,
.markdown-content td {
border: 1px solid rgba(0, 0, 0, 0.15);
padding: 6px 12px;
text-align: left;
}
.markdown-content blockquote {
border-left: 4px solid rgba(0, 0, 0, 0.15);
padding-left: 10px;
margin: 0.5em 0 0.5em 0.5em;
color: rgba(0, 0, 0, 0.6);
}
[data-theme="dark"] .markdown-content blockquote {
border-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.8);
}
.markdown-content hr {
border: none;
border-top: 1px solid rgba(0, 0, 0, 0.15);
margin: 0.75em 0;
}
[data-theme="dark"] .markdown-content pre,
[data-theme="dark"] .markdown-content code {
background-color: rgba(255, 255, 255, 0.07);
}
.brand-mark {
letter-spacing: 0.02em;
}
.reference-tooltip {
@apply bg-base-100 text-base-content border-2 border-neutral p-3 text-sm w-72 max-w-xs;
position: fixed;
z-index: 9999;
box-shadow: var(--nb-shadow);
}
}
/* Theme-aware placeholder contrast tweaks */
@layer base {
/* Light theme keeps default neutral tone via utilities */
[data-theme="dark"] .nb-input::placeholder,
[data-theme="dark"] .input::placeholder,
[data-theme="dark"] .textarea::placeholder,
[data-theme="dark"] textarea::placeholder,
[data-theme="dark"] input::placeholder {
color: rgba(255, 255, 255, 0.78) !important;
opacity: 0.85;
}
}
/* satoshi.css */
@@ -58,4 +610,28 @@
font-weight: 300 900;
font-style: italic;
font-display: swap;
}
/* Minimal override: prevent DaisyUI .menu hover bg on our nb buttons */
@layer utilities {
/* Let plain nb-btns remain transparent on hover within menus */
.menu li>.nb-btn:hover {
background-color: transparent;
}
/* Keep CTA background on hover within menus */
.menu li>.nb-cta:hover {
background-color: var(--color-accent);
color: var(--color-accent-content);
}
.toast-alert {
@apply mt-2 flex flex-col text-left gap-1;
box-shadow: var(--nb-shadow);
}
.toast-alert-title {
@apply text-lg font-bold;
}
}

View File

@@ -73,29 +73,29 @@
function attachOverlay(container, { onSearch, onToggleLabels, onCenter }) {
const overlay = document.createElement('div');
overlay.className = 'absolute top-2 left-2 z-10 flex gap-2 items-center';
overlay.className = 'kg-overlay';
// search box
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Search nodes…';
input.className = 'input input-sm input-bordered';
input.className = 'nb-input kg-search-input';
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') onSearch && onSearch(input.value.trim());
});
const searchBtn = document.createElement('button');
searchBtn.className = 'btn btn-sm';
searchBtn.className = 'nb-btn btn-xs nb-cta';
searchBtn.textContent = 'Go';
searchBtn.addEventListener('click', () => onSearch && onSearch(input.value.trim()));
const labelToggle = document.createElement('button');
labelToggle.className = 'btn btn-sm';
labelToggle.className = 'nb-btn btn-xs';
labelToggle.textContent = 'Labels';
labelToggle.addEventListener('click', () => onToggleLabels && onToggleLabels());
const centerBtn = document.createElement('button');
centerBtn.className = 'btn btn-sm';
centerBtn.className = 'nb-btn btn-xs';
centerBtn.textContent = 'Center';
centerBtn.addEventListener('click', () => onCenter && onCenter());
@@ -112,15 +112,15 @@
function attachLegends(container, typeColor, relColor) {
const wrap = document.createElement('div');
wrap.className = 'absolute bottom-2 left-2 z-10 flex gap-6 flex-wrap';
wrap.className = 'kg-legend';
function section(title, items) {
const sec = document.createElement('div');
sec.className = 'rounded-box bg-base-100/80 backdrop-blur shadow p-2';
const h = document.createElement('div'); h.className = 'text-xs opacity-70 mb-1'; h.textContent = title; sec.appendChild(h);
sec.className = 'nb-card kg-legend-card';
const h = document.createElement('div'); h.className = 'kg-legend-heading'; h.textContent = title; sec.appendChild(h);
items.forEach(([label, color]) => {
const row = document.createElement('div'); row.className = 'flex items-center gap-2 text-xs';
const sw = document.createElement('span'); sw.style.background = color; sw.style.width = '10px'; sw.style.height = '10px'; sw.style.borderRadius = '9999px';
const row = document.createElement('div'); row.className = 'kg-legend-row';
const sw = document.createElement('span'); sw.style.background = color; sw.style.width = '12px'; sw.style.height = '12px'; sw.style.border = '2px solid #000';
const t = document.createElement('span'); t.textContent = label || '—';
row.appendChild(sw); row.appendChild(t); sec.appendChild(row);
});

File diff suppressed because one or more lines are too long

View File

@@ -6,33 +6,31 @@
return;
}
const alert = document.createElement('div');
// Base classes for the alert
alert.className = `alert alert-${type} mt-2 shadow-md flex flex-col text-start`;
alert.className = `alert toast-alert alert-${type}`;
alert.style.opacity = '1';
alert.style.transition = 'opacity 0.5s ease-out';
// Build inner HTML based on whether title is provided
let innerHTML = '';
if (title) {
innerHTML += `<div class="font-bold text-lg">${title}</div>`; // Title element
innerHTML += `<div>${description}</div>`; // Description element
} else {
// Structure without title
innerHTML += `<span>${description}</span>`;
const titleEl = document.createElement('div');
titleEl.className = 'toast-alert-title';
titleEl.textContent = title;
alert.appendChild(titleEl);
}
alert.innerHTML = innerHTML;
const bodyEl = document.createElement(title ? 'div' : 'span');
bodyEl.textContent = description;
alert.appendChild(bodyEl);
container.appendChild(alert);
// Auto-remove after a delay
setTimeout(() => {
// Optional: Add fade-out effect
alert.style.opacity = '0';
alert.style.transition = 'opacity 0.5s ease-out';
setTimeout(() => alert.remove(), 500); // Remove after fade
}, 3000); // Start fade-out after 3 seconds
setTimeout(() => alert.remove(), 500);
}, 3000);
};
document.body.addEventListener('toast', function (event) {
console.log(event);
// Extract data from the event detail, matching the Rust payload
const detail = event.detail;
if (detail && detail.description) {
@@ -54,4 +52,3 @@
if (container) container.innerHTML = '';
});
})

View File

@@ -1,7 +1,6 @@
{
"name": "html-router",
"version": "1.0.0",
"main": "tailwind.config.js",
"scripts": {
"tailwind": "npx @tailwindcss/cli -i app.css -o assets/style.css -w -m"
},
@@ -14,4 +13,4 @@
"daisyui": "^5.0.12",
"tailwindcss": "^4.1.2"
}
}
}

View File

@@ -35,5 +35,6 @@ where
.add_protected_routes(routes::content::router())
.add_protected_routes(routes::knowledge::router())
.add_protected_routes(routes::ingestion::router())
.with_compression()
.build()
}

View File

@@ -0,0 +1,7 @@
use tower_http::compression::CompressionLayer;
/// Provides a default compression layer that negotiates encoding based on the
/// `Accept-Encoding` header of the incoming request.
pub fn compression_layer() -> CompressionLayer {
CompressionLayer::new()
}

View File

@@ -1,3 +1,4 @@
pub mod analytics_middleware;
pub mod auth_middleware;
pub mod compression;
pub mod response_middleware;

View File

@@ -13,7 +13,7 @@ use crate::{
html_state::HtmlState,
middlewares::{
analytics_middleware::analytics_middleware, auth_middleware::require_auth,
response_middleware::with_template_response,
compression::compression_layer, response_middleware::with_template_response,
},
};
@@ -48,6 +48,7 @@ pub struct RouterFactory<S> {
nested_protected_routes: Vec<(String, Router<S>)>,
custom_middleware: MiddleWareVecType<S>,
public_assets_config: Option<AssetsConfig>,
compression_enabled: bool,
}
struct AssetsConfig {
@@ -69,6 +70,7 @@ where
nested_protected_routes: Vec::new(),
custom_middleware: Vec::new(),
public_assets_config: None,
compression_enabled: false,
}
}
@@ -115,6 +117,12 @@ where
self
}
/// Enables response compression when building the router.
pub fn with_compression(mut self) -> Self {
self.compression_enabled = true;
self
}
pub fn build(self) -> Router<S> {
// Start with an empty router
let mut public_router = Router::new();
@@ -169,21 +177,26 @@ where
}
// Apply common middleware
router = router.layer(from_fn_with_state(
self.app_state.clone(),
analytics_middleware::<HtmlState>,
));
router = router.layer(map_response_with_state(
self.app_state.clone(),
with_template_response::<HtmlState>,
));
router = router.layer(
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
self.app_state.db.client.clone(),
))
.with_config(AuthConfig::<String>::default()),
);
router = router.layer(SessionLayer::new((*self.app_state.session_store).clone()));
if self.compression_enabled {
router = router.layer(compression_layer());
}
router
.layer(from_fn_with_state(
self.app_state.clone(),
analytics_middleware::<HtmlState>,
))
.layer(map_response_with_state(
self.app_state.clone(),
with_template_response::<HtmlState>,
))
.layer(
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
self.app_state.db.client.clone(),
))
.with_config(AuthConfig::<String>::default()),
)
.layer(SessionLayer::new((*self.app_state.session_store).clone()))
}
}

View File

@@ -406,4 +406,4 @@ pub async fn patch_image_prompt(
settings: new_settings,
},
))
}
}

View File

@@ -3,11 +3,12 @@ use axum::{
response::IntoResponse,
Form,
};
use axum_htmx::{HxBoosted, HxRequest};
use axum_htmx::{HxBoosted, HxRequest, HxTarget};
use serde::{Deserialize, Serialize};
use common::storage::types::{
conversation::Conversation, file_info::FileInfo, text_content::TextContent, user::User, knowledge_entity::KnowledgeEntity, text_chunk::TextChunk,
conversation::Conversation, file_info::FileInfo, knowledge_entity::KnowledgeEntity,
text_chunk::TextChunk, text_content::TextContent, user::User,
};
use crate::{
@@ -27,6 +28,12 @@ pub struct ContentPageData {
conversation_archive: Vec<Conversation>,
}
#[derive(Serialize)]
pub struct RecentTextContentData {
pub user: User,
pub text_contents: Vec<TextContent>,
}
#[derive(Deserialize)]
pub struct FilterParams {
category: Option<String>,
@@ -102,12 +109,25 @@ pub async fn patch_text_content(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
Path(id): Path<String>,
HxTarget(target): HxTarget,
Form(form): Form<PatchTextContentParams>,
) -> Result<impl IntoResponse, HtmlError> {
User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
TextContent::patch(&id, &form.context, &form.category, &form.text, &state.db).await?;
if target.as_deref() == Some("latest_content_section") {
let text_contents = User::get_latest_text_contents(&user.id, &state.db).await?;
return Ok(TemplateResponse::new_template(
"dashboard/recent_content.html",
RecentTextContentData {
user,
text_contents,
},
));
}
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
let categories = User::get_user_categories(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
@@ -187,12 +207,6 @@ pub async fn show_recent_content(
) -> Result<impl IntoResponse, HtmlError> {
let text_contents = User::get_latest_text_contents(&user.id, &state.db).await?;
#[derive(Serialize)]
pub struct RecentTextContentData {
pub user: User,
pub text_contents: Vec<TextContent>,
}
Ok(TemplateResponse::new_template(
"dashboard/recent_content.html",
RecentTextContentData {

View File

@@ -4,6 +4,7 @@ use axum::{
http::{header, HeaderMap, HeaderValue, StatusCode},
response::IntoResponse,
};
use futures::try_join;
use serde::Serialize;
use tokio::join;
@@ -14,6 +15,8 @@ use crate::{
},
AuthSessionType,
};
use common::storage::store;
use common::storage::types::user::DashboardStats;
use common::{
error::AppError,
storage::types::{
@@ -22,7 +25,6 @@ use common::{
text_chunk::TextChunk, text_content::TextContent, user::User,
},
};
use common::storage::store;
use crate::html_state::HtmlState;
@@ -30,6 +32,7 @@ use crate::html_state::HtmlState;
pub struct IndexPageData {
user: Option<User>,
text_contents: Vec<TextContent>,
stats: DashboardStats,
active_jobs: Vec<IngestionTask>,
conversation_archive: Vec<Conversation>,
}
@@ -42,19 +45,21 @@ pub async fn index_handler(
return Ok(TemplateResponse::redirect("/signin"));
};
let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db).await?;
let text_contents = User::get_latest_text_contents(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let (text_contents, conversation_archive, stats, active_jobs) = try_join!(
User::get_latest_text_contents(&user.id, &state.db),
User::get_user_conversations(&user.id, &state.db),
User::get_dashboard_stats(&user.id, &state.db),
User::get_unfinished_ingestion_tasks(&user.id, &state.db)
)?;
Ok(TemplateResponse::new_template(
"dashboard/base.html",
IndexPageData {
user: Some(user),
text_contents,
active_jobs,
stats,
conversation_archive,
active_jobs,
},
))
}
@@ -153,9 +158,8 @@ pub async fn show_active_jobs(
) -> Result<impl IntoResponse, HtmlError> {
let active_jobs = User::get_unfinished_ingestion_tasks(&user.id, &state.db).await?;
Ok(TemplateResponse::new_partial(
Ok(TemplateResponse::new_template(
"dashboard/active_jobs.html",
"active_jobs_section",
ActiveJobsData {
user: user.clone(),
active_jobs,

View File

@@ -5,6 +5,7 @@ use axum::{
response::IntoResponse,
};
use common::storage::types::{
conversation::Conversation,
text_content::{TextContent, TextContentSearchResult},
user::User,
};
@@ -47,7 +48,9 @@ pub async fn search_result_handler(
search_result: Vec<TextContentSearchResult>,
query_param: String,
user: User,
conversation_archive: Vec<Conversation>,
}
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let (search_results_for_template, final_query_param_for_template) =
if let Some(actual_query) = params.query {
@@ -72,6 +75,7 @@ pub async fn search_result_handler(
search_result: search_results_for_template,
query_param: final_query_param_for_template,
user,
conversation_archive,
},
))
}

View File

@@ -1,33 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./templates/**/*',
],
theme: {
container: {
padding: {
DEFAULT: '10px',
sm: '2rem',
lg: '4rem',
xl: '5rem',
'2xl': '6rem',
},
},
extend: {
fontFamily: {
satoshi: ['Satoshi', 'sans-serif'],
},
typography: {
DEFAULT: {
css: {
maxWidth: '90ch', // Override max-width for all prose instances
},
},
},
},
},
daisyui: {
themes: ["light", "dark"],
},
}

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>