mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-21 08:21:25 +02:00
design: neobrutalist_theme into main
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 = '';
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
7
html-router/src/middlewares/compression.rs
Normal file
7
html-router/src/middlewares/compression.rs
Normal 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()
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod analytics_middleware;
|
||||
pub mod auth_middleware;
|
||||
pub mod compression;
|
||||
pub mod response_middleware;
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,4 +406,4 @@ pub async fn patch_image_prompt(
|
||||
settings: new_settings,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
Don’t 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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
25
html-router/templates/dashboard/statistics.html
Normal file
25
html-router/templates/dashboard/statistics.html
Normal 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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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='') }}">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user