mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-23 01:08:33 +02:00
feat: release build bundles assets
This commit is contained in:
@@ -4,7 +4,7 @@ use super::types::{analytics::Analytics, system_settings::SystemSettings, Stored
|
||||
use axum_session::{SessionConfig, SessionError, SessionStore};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use futures::Stream;
|
||||
use std::ops::Deref;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
use surrealdb::{
|
||||
engine::any::{connect, Any},
|
||||
opt::auth::Root,
|
||||
@@ -15,6 +15,9 @@ use surrealdb::{
|
||||
pub struct SurrealDbClient {
|
||||
pub client: Surreal<Any>,
|
||||
}
|
||||
pub trait ProvidesDb {
|
||||
fn db(&self) -> &Arc<SurrealDbClient>;
|
||||
}
|
||||
|
||||
impl SurrealDbClient {
|
||||
/// # Initialize a new datbase client
|
||||
|
||||
@@ -4,6 +4,10 @@ pub use minijinja_contrib;
|
||||
pub use minijinja_embed;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait ProvidesTemplateEngine {
|
||||
fn template_engine(&self) -> &Arc<TemplateEngine>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TemplateEngine {
|
||||
// Use AutoReload for debug builds (debug_assertions is true)
|
||||
|
||||
@@ -30,6 +30,8 @@ plotly = "0.12.1"
|
||||
surrealdb = "2.0.4"
|
||||
tower-http = { version = "0.6.2", features = ["fs"] }
|
||||
chrono-tz = "0.10.1"
|
||||
tower-serve-static = "0.1.1"
|
||||
include_dir = "0.7.4"
|
||||
|
||||
common = { path = "../common" }
|
||||
composite-retrieval = { path = "../composite-retrieval" }
|
||||
|
||||
BIN
crates/html-router/assets/fonts/Satoshi-Regular.ttf
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Regular.ttf
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-Regular.woff
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Regular.woff
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-Regular.woff2
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Regular.woff2
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-Variable.eot
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Variable.eot
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-Variable.ttf
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Variable.ttf
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-Variable.woff
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Variable.woff
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-Variable.woff2
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-Variable.woff2
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.eot
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.eot
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.ttf
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.ttf
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.woff
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.woff
Normal file
Binary file not shown.
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.woff2
Normal file
BIN
crates/html-router/assets/fonts/Satoshi-VariableItalic.woff2
Normal file
Binary file not shown.
1
crates/html-router/assets/htmx-ext-sse.js
Normal file
1
crates/html-router/assets/htmx-ext-sse.js
Normal file
@@ -0,0 +1 @@
|
||||
(function(){var g;htmx.defineExtension("sse",{init:function(e){g=e;if(htmx.createEventSource==undefined){htmx.createEventSource=t}},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(e,t){var r=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var n=g.getInternalData(r);var s=n.sseEventSource;if(s){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"nodeReplaced"});n.sseEventSource.close()}return;case"htmx:afterProcessNode":i(r)}}});function t(e){return new EventSource(e,{withCredentials:true})}function a(n){if(g.getAttributeValue(n,"sse-swap")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var t=g.getAttributeValue(n,"sse-swap");var r=t.split(",");for(var i=0;i<r.length;i++){const u=r[i].trim();const c=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(u,c);return}if(!g.triggerEvent(n,"htmx:sseBeforeMessage",e)){return}f(n,e.data);g.triggerEvent(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=c;a.addEventListener(u,c)}}if(g.getAttributeValue(n,"hx-trigger")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var o=g.getTriggerSpecs(n);o.forEach(function(t){if(t.trigger.slice(0,4)!=="sse:"){return}var r=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(t.trigger.slice(4),r)}htmx.trigger(n,t.trigger,e);htmx.trigger(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=r;a.addEventListener(t.trigger.slice(4),r)})}}function i(e,t){if(e==null){return null}if(g.getAttributeValue(e,"sse-connect")){var r=g.getAttributeValue(e,"sse-connect");if(r==null){return}n(e,r,t)}a(e)}function n(r,e,n){var s=htmx.createEventSource(e);s.onerror=function(e){g.triggerErrorEvent(r,"htmx:sseError",{error:e,source:s});if(l(r)){return}if(s.readyState===EventSource.CLOSED){n=n||0;n=Math.max(Math.min(n*2,128),1);var t=n*500;window.setTimeout(function(){i(r,n)},t)}};s.onopen=function(e){g.triggerEvent(r,"htmx:sseOpen",{source:s});if(n&&n>0){const t=r.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let e=0;e<t.length;e++){a(t[e])}n=0}};g.getInternalData(r).sseEventSource=s;var t=g.getAttributeValue(r,"sse-close");if(t){s.addEventListener(t,function(){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"message"});s.close()})}}function l(e){if(!g.bodyContains(e)){var t=g.getInternalData(e).sseEventSource;if(t!=undefined){g.triggerEvent(e,"htmx:sseClose",{source:t,type:"nodeMissing"});t.close();return true}}return false}function f(t,r){g.withExtensions(t,function(e){r=e.transformResponse(r,null,t)});var e=g.getSwapSpecification(t);var n=g.getTarget(t);g.swap(n,r,e)}function v(e){return g.getInternalData(e).sseEventSource!=null}})();
|
||||
1
crates/html-router/assets/htmx-sse.min.js
vendored
Normal file
1
crates/html-router/assets/htmx-sse.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(function(){var g;htmx.defineExtension("sse",{init:function(e){g=e;if(htmx.createEventSource==undefined){htmx.createEventSource=t}},getSelectors:function(){return["[sse-connect]","[data-sse-connect]","[sse-swap]","[data-sse-swap]"]},onEvent:function(e,t){var r=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var n=g.getInternalData(r);var s=n.sseEventSource;if(s){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"nodeReplaced"});n.sseEventSource.close()}return;case"htmx:afterProcessNode":i(r)}}});function t(e){return new EventSource(e,{withCredentials:true})}function a(n){if(g.getAttributeValue(n,"sse-swap")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var t=g.getAttributeValue(n,"sse-swap");var r=t.split(",");for(var i=0;i<r.length;i++){const u=r[i].trim();const c=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(u,c);return}if(!g.triggerEvent(n,"htmx:sseBeforeMessage",e)){return}f(n,e.data);g.triggerEvent(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=c;a.addEventListener(u,c)}}if(g.getAttributeValue(n,"hx-trigger")){var s=g.getClosestMatch(n,v);if(s==null){return null}var e=g.getInternalData(s);var a=e.sseEventSource;var o=g.getTriggerSpecs(n);o.forEach(function(t){if(t.trigger.slice(0,4)!=="sse:"){return}var r=function(e){if(l(s)){return}if(!g.bodyContains(n)){a.removeEventListener(t.trigger.slice(4),r)}htmx.trigger(n,t.trigger,e);htmx.trigger(n,"htmx:sseMessage",e)};g.getInternalData(n).sseEventListener=r;a.addEventListener(t.trigger.slice(4),r)})}}function i(e,t){if(e==null){return null}if(g.getAttributeValue(e,"sse-connect")){var r=g.getAttributeValue(e,"sse-connect");if(r==null){return}n(e,r,t)}a(e)}function n(r,e,n){var s=htmx.createEventSource(e);s.onerror=function(e){g.triggerErrorEvent(r,"htmx:sseError",{error:e,source:s});if(l(r)){return}if(s.readyState===EventSource.CLOSED){n=n||0;n=Math.max(Math.min(n*2,128),1);var t=n*500;window.setTimeout(function(){i(r,n)},t)}};s.onopen=function(e){g.triggerEvent(r,"htmx:sseOpen",{source:s});if(n&&n>0){const t=r.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]");for(let e=0;e<t.length;e++){a(t[e])}n=0}};g.getInternalData(r).sseEventSource=s;var t=g.getAttributeValue(r,"sse-close");if(t){s.addEventListener(t,function(){g.triggerEvent(r,"htmx:sseClose",{source:s,type:"message"});s.close()})}}function l(e){if(!g.bodyContains(e)){var t=g.getInternalData(e).sseEventSource;if(t!=undefined){g.triggerEvent(e,"htmx:sseClose",{source:t,type:"nodeMissing"});t.close();return true}}return false}function f(t,r){g.withExtensions(t,function(e){r=e.transformResponse(r,null,t)});var e=g.getSwapSpecification(t);var n=g.getTarget(t);g.swap(n,r,e)}function v(e){return g.getInternalData(e).sseEventSource!=null}})();
|
||||
1
crates/html-router/assets/htmx.min.js
vendored
Normal file
1
crates/html-router/assets/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
crates/html-router/assets/icon/android-chrome-192x192.png
Normal file
BIN
crates/html-router/assets/icon/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
crates/html-router/assets/icon/android-chrome-512x512.png
Normal file
BIN
crates/html-router/assets/icon/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
BIN
crates/html-router/assets/icon/apple-touch-icon.png
Normal file
BIN
crates/html-router/assets/icon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
crates/html-router/assets/icon/favicon-16x16.png
Normal file
BIN
crates/html-router/assets/icon/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 790 B |
BIN
crates/html-router/assets/icon/favicon-32x32.png
Normal file
BIN
crates/html-router/assets/icon/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
crates/html-router/assets/icon/favicon.ico
Normal file
BIN
crates/html-router/assets/icon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
crates/html-router/assets/icon/site.webmanifest
Normal file
1
crates/html-router/assets/icon/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
61
crates/html-router/assets/input.css
Normal file
61
crates/html-router/assets/input.css
Normal file
@@ -0,0 +1,61 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@plugin "daisyui" {
|
||||
exclude: rootscrollbargutter;
|
||||
}
|
||||
|
||||
@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 {
|
||||
body {
|
||||
@apply font-satoshi;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
/* satoshi.css */
|
||||
@font-face {
|
||||
font-family: 'Satoshi';
|
||||
src: url('fonts/Satoshi-Variable.woff2') format('woff2'),
|
||||
url('fonts/Satoshi-Variable.woff') format('woff'),
|
||||
url('fonts/Satoshi-Variable.ttf') format('truetype');
|
||||
font-weight: 300 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Satoshi';
|
||||
src: url('fonts/Satoshi-VariableItalic.woff2') format('woff2'),
|
||||
url('fonts/Satoshi-VariableItalic.woff') format('woff'),
|
||||
url('fonts/Satoshi-VariableItalic.ttf') format('truetype');
|
||||
font-weight: 300 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
13
crates/html-router/assets/manifest.json
Normal file
13
crates/html-router/assets/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "minne",
|
||||
"short_name": "minne",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
5974
crates/html-router/assets/style.css
Normal file
5974
crates/html-router/assets/style.css
Normal file
File diff suppressed because it is too large
Load Diff
32
crates/html-router/assets/theme-toggle.js
Normal file
32
crates/html-router/assets/theme-toggle.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const initializeTheme = () => {
|
||||
const themeToggle = document.querySelector('.theme-controller');
|
||||
if (!themeToggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Initialize theme from local storage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const initialTheme = savedTheme ? savedTheme : (prefersDark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||||
themeToggle.checked = initialTheme === 'dark';
|
||||
|
||||
// Update theme and local storage on toggle
|
||||
themeToggle.addEventListener('change', () => {
|
||||
const theme = themeToggle.checked ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Run the initialization after the DOM is fully loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeTheme();
|
||||
});
|
||||
|
||||
// Reinitialize theme toggle after HTMX swaps
|
||||
document.addEventListener('htmx:afterSwap', initializeTheme);
|
||||
document.addEventListener('htmx:afterSettle', initializeTheme);
|
||||
@@ -1,48 +1,43 @@
|
||||
use axum_session::SessionStore;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use common::create_template_engine;
|
||||
use common::storage::db::SurrealDbClient;
|
||||
use common::utils::config::AppConfig;
|
||||
use common::utils::template_engine::TemplateEngine;
|
||||
use common::utils::template_engine::{ProvidesTemplateEngine, TemplateEngine};
|
||||
use common::{create_template_engine, storage::db::ProvidesDb};
|
||||
use std::sync::Arc;
|
||||
use surrealdb::engine::any::Any;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{OpenAIClientType, SessionStoreType};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HtmlState {
|
||||
pub db: Arc<SurrealDbClient>,
|
||||
pub openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
|
||||
pub openai_client: Arc<OpenAIClientType>,
|
||||
pub templates: Arc<TemplateEngine>,
|
||||
pub session_store: Arc<SessionStore<SessionSurrealPool<Any>>>,
|
||||
pub session_store: Arc<SessionStoreType>,
|
||||
}
|
||||
|
||||
impl HtmlState {
|
||||
pub async fn new(config: &AppConfig) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub fn new_with_resources(
|
||||
db: Arc<SurrealDbClient>,
|
||||
openai_client: Arc<OpenAIClientType>,
|
||||
session_store: Arc<SessionStoreType>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let template_engine = create_template_engine!("templates");
|
||||
debug!("Template engine created for html_router.");
|
||||
|
||||
let surreal_db_client = Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
surreal_db_client.ensure_initialized().await?;
|
||||
|
||||
let openai_client = Arc::new(async_openai::Client::new());
|
||||
|
||||
let session_store = Arc::new(surreal_db_client.create_session_store().await?);
|
||||
|
||||
let app_state = HtmlState {
|
||||
db: surreal_db_client.clone(),
|
||||
templates: Arc::new(template_engine),
|
||||
openai_client: openai_client.clone(),
|
||||
Ok(Self {
|
||||
db,
|
||||
openai_client,
|
||||
session_store,
|
||||
};
|
||||
|
||||
Ok(app_state)
|
||||
templates: Arc::new(template_engine),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ProvidesDb for HtmlState {
|
||||
fn db(&self) -> &Arc<SurrealDbClient> {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
impl ProvidesTemplateEngine for HtmlState {
|
||||
fn template_engine(&self) -> &Arc<TemplateEngine> {
|
||||
&self.templates
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod router_factory;
|
||||
pub mod routes;
|
||||
|
||||
use axum::{extract::FromRef, Router};
|
||||
use axum_session::Session;
|
||||
use axum_session::{Session, SessionStore};
|
||||
use axum_session_auth::AuthSession;
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use common::storage::types::user::User;
|
||||
@@ -14,6 +14,8 @@ use surrealdb::{engine::any::Any, Surreal};
|
||||
|
||||
pub type AuthSessionType = AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>;
|
||||
pub type SessionType = Session<SessionSurrealPool<Any>>;
|
||||
pub type SessionStoreType = SessionStore<SessionSurrealPool<Any>>;
|
||||
pub type OpenAIClientType = async_openai::Client<async_openai::config::OpenAIConfig>;
|
||||
|
||||
/// Html routes
|
||||
pub fn html_routes<S>(app_state: &HtmlState) -> Router<S>
|
||||
|
||||
@@ -3,31 +3,28 @@ use axum::{
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use surrealdb::engine::any::Any;
|
||||
|
||||
use common::storage::types::analytics::Analytics;
|
||||
use common::storage::{db::ProvidesDb, types::analytics::Analytics};
|
||||
|
||||
use crate::html_state::HtmlState;
|
||||
use crate::SessionType;
|
||||
|
||||
pub async fn analytics_middleware(
|
||||
State(state): State<HtmlState>,
|
||||
session: axum_session::Session<SessionSurrealPool<Any>>,
|
||||
/// Middleware to count unique visitors and page loads
|
||||
pub async fn analytics_middleware<S>(
|
||||
State(state): State<S>,
|
||||
session: SessionType,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Get the path from the request
|
||||
) -> Response
|
||||
where
|
||||
S: ProvidesDb + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let path = request.uri().path();
|
||||
|
||||
// Only count if it's a main page request (not assets or other resources)
|
||||
if !path.starts_with("/assets") && !path.starts_with("/_next") && !path.contains('.') {
|
||||
if !path.starts_with("/assets") && !path.contains('.') {
|
||||
if !session.get::<bool>("counted_visitor").unwrap_or(false) {
|
||||
let _ = Analytics::increment_visitors(&state.db).await;
|
||||
let _ = Analytics::increment_visitors(state.db()).await;
|
||||
session.set("counted_visitor", true);
|
||||
}
|
||||
|
||||
let _ = Analytics::increment_page_loads(&state.db).await;
|
||||
let _ = Analytics::increment_page_loads(state.db()).await;
|
||||
}
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::html_state::HtmlState;
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
Extension,
|
||||
};
|
||||
use common::error::AppError;
|
||||
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine};
|
||||
use minijinja::{context, Value};
|
||||
use serde::Serialize;
|
||||
use tracing::error;
|
||||
@@ -106,58 +105,45 @@ impl IntoResponse for TemplateResponse {
|
||||
}
|
||||
}
|
||||
|
||||
struct TemplateStateWrapper {
|
||||
state: HtmlState,
|
||||
template_response: TemplateResponse,
|
||||
}
|
||||
pub async fn with_template_response<S>(State(state): State<S>, response: Response) -> Response
|
||||
where
|
||||
S: ProvidesTemplateEngine + Clone + Send + Sync + 'static,
|
||||
{
|
||||
if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() {
|
||||
let template_engine = state.template_engine();
|
||||
|
||||
impl IntoResponse for TemplateStateWrapper {
|
||||
fn into_response(self) -> Response {
|
||||
let template_engine = &self.state.templates;
|
||||
|
||||
match &self.template_response.template_kind {
|
||||
match &template_response.template_kind {
|
||||
TemplateKind::Full(name) => {
|
||||
match template_engine.render(name, &self.template_response.context) {
|
||||
match template_engine.render(name, &template_response.context) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render template: {:?}", e);
|
||||
error!("Failed to render template '{}': {:?}", name, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, fallback_error()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateKind::Partial(template, block) => {
|
||||
match template_engine.render_block(template, block, &self.template_response.context)
|
||||
{
|
||||
match template_engine.render_block(template, block, &template_response.context) {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render block: {:?}", e);
|
||||
error!("Failed to render block '{}/{}': {:?}", template, block, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, fallback_error()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateKind::Error(status) => {
|
||||
match template_engine.render("errors/error.html", &self.template_response.context) {
|
||||
match template_engine.render("errors/error.html", &template_response.context) {
|
||||
Ok(html) => (*status, Html(html)).into_response(),
|
||||
Err(_) => (*status, fallback_error()).into_response(),
|
||||
Err(e) => {
|
||||
error!("Failed to render error template: {:?}", e);
|
||||
(*status, fallback_error()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
TemplateKind::Redirect(path) => {
|
||||
(StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn with_template_response(
|
||||
State(state): State<HtmlState>,
|
||||
response: Response,
|
||||
) -> Response {
|
||||
if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() {
|
||||
TemplateStateWrapper {
|
||||
state,
|
||||
template_response,
|
||||
}
|
||||
.into_response()
|
||||
} else {
|
||||
response
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
use common::storage::types::user::User;
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::{
|
||||
html_state::HtmlState,
|
||||
@@ -18,6 +17,27 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! create_asset_service {
|
||||
// Takes the relative path to the asset directory
|
||||
($relative_path:expr) => {{
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let assets_path = crate_dir.join($relative_path);
|
||||
tracing::debug!("Assets: Serving from filesystem: {:?}", assets_path);
|
||||
tower_http::services::ServeDir::new(assets_path)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
tracing::debug!("Assets: Serving embedded directory");
|
||||
static ASSETS_DIR: include_dir::Dir<'static> =
|
||||
include_dir::include_dir!("$CARGO_MANIFEST_DIR/assets");
|
||||
tower_serve_static::ServeDir::new(&ASSETS_DIR)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub struct RouterFactory<S> {
|
||||
app_state: HtmlState,
|
||||
public_routers: Vec<Router<S>>,
|
||||
@@ -108,27 +128,35 @@ where
|
||||
}
|
||||
|
||||
// Add public assets to public router
|
||||
if let Some(assets) = self.public_assets_config {
|
||||
public_router =
|
||||
public_router.nest_service(&assets.path, ServeDir::new(assets.directory));
|
||||
if let Some(assets_config) = self.public_assets_config {
|
||||
// Call the macro using the stored relative directory path
|
||||
let asset_service = create_asset_service!(&assets_config.directory);
|
||||
// Nest the resulting service under the stored URL path
|
||||
public_router = public_router.nest_service(&assets_config.path, asset_service);
|
||||
}
|
||||
|
||||
// Start with an empty protected router
|
||||
let mut protected_router = Router::new();
|
||||
|
||||
// Merge all protected routers
|
||||
// Check if there are any protected routers
|
||||
let has_protected_routes =
|
||||
!self.protected_routers.is_empty() || !self.nested_protected_routes.is_empty();
|
||||
|
||||
// Merge root-level protected routers
|
||||
for router in self.protected_routers {
|
||||
protected_router = protected_router.merge(router);
|
||||
}
|
||||
|
||||
// Add nested protected routes
|
||||
// Nest protected routers
|
||||
for (path, router) in self.nested_protected_routes {
|
||||
protected_router = protected_router.nest(&path, router);
|
||||
}
|
||||
|
||||
// Apply auth middleware to all protected routes
|
||||
let protected_router =
|
||||
protected_router.route_layer(from_fn_with_state(self.app_state.clone(), require_auth));
|
||||
// Apply auth middleware
|
||||
if has_protected_routes {
|
||||
protected_router = protected_router
|
||||
.route_layer(from_fn_with_state(self.app_state.clone(), require_auth));
|
||||
}
|
||||
|
||||
// Combine public and protected routes
|
||||
let mut router = Router::new().merge(public_router).merge(protected_router);
|
||||
@@ -142,11 +170,11 @@ where
|
||||
router
|
||||
.layer(from_fn_with_state(
|
||||
self.app_state.clone(),
|
||||
analytics_middleware,
|
||||
analytics_middleware::<HtmlState>,
|
||||
))
|
||||
.layer(map_response_with_state(
|
||||
self.app_state.clone(),
|
||||
with_template_response,
|
||||
with_template_response::<HtmlState>,
|
||||
))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends "body_base.html" %}
|
||||
{% block main %}
|
||||
<main class="flex justify-center grow mt-2 sm:mt-4 pb-10">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-4">
|
||||
<!-- Documentation Menu -->
|
||||
<aside class="bg-base-200 rounded-lg p-4">
|
||||
{% include "documentation/menu.html" %}
|
||||
</aside>
|
||||
<!-- Main Content -->
|
||||
|
||||
<article
|
||||
class="prose prose-sm md:prose-base prose-h1:mb-2 prose-h2:my-2 prose-p:my-2 prose-ul:my-2 prose-pre:my-2 flex mx-auto justify-center flex-col">
|
||||
{% block article %}
|
||||
{% endblock %}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -1,58 +0,0 @@
|
||||
{% extends 'documentation/base.html' %}
|
||||
{% block article %}
|
||||
<h1>Get Started with Minne</h1>
|
||||
<p>Minne offers two installation options to suit your needs:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Hosted Version:</strong> Enjoy a hassle‐free experience by signing up for the ready‐to‐use service.
|
||||
Simply navigate to <code>/signup</code> to create an account.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Self-Hosted:</strong> Gain full control by running Minne on your own infrastructure. Visit
|
||||
<a href="https://github.com/perstarkse/minne">GitHub</a> to download the latest release. After extracting the
|
||||
release, open the <code>config.yaml</code> file and set the following configurations:
|
||||
</li>
|
||||
</ol>
|
||||
<pre class="overflow-x-auto text-sm">
|
||||
<code class="break-words whitespace-pre-wrap">
|
||||
OPENAI_API_KEY: your_api_key
|
||||
DB_ADDRESS: your_db_address
|
||||
DB_USER: your_db_user
|
||||
DB_PASSWORD: your_db_password
|
||||
</code>
|
||||
</pre>
|
||||
<p>The database settings relate to a running instance of SurrealDB. You can opt for their cloud solution or run your
|
||||
own instance.</p>
|
||||
|
||||
<p>Once your configuration is complete, start both the server and the worker. They can be hosted on separate
|
||||
machines, with different resource requirements:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Server:</strong> Lightweight. A minimum of 1 core and 256MB of RAM is recommended.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Worker:</strong> Handles content parsing and creation of database entities. It's recommended to allocate at
|
||||
least two cores and 1024 MB RAM. It will run on less but might run into constraints depending on the content being
|
||||
parsed.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>After launching the services, navigate to <code><your_url>:3000/signup</code> to register. The first
|
||||
account created will automatically receive admin permissions, allowing you to later disable further registrations
|
||||
via the <code>/admin</code> page if desired.</p>
|
||||
|
||||
<p>From the homepage (<code>/</code>), you can:</p>
|
||||
|
||||
<ul>
|
||||
<li>Submit content, including files, videos, and URLs for ingestion.</li>
|
||||
<li>Monitor job statuses and manage your existing content.</li>
|
||||
<li>Search your content or start a chat conversation for assistance.</li>
|
||||
</ul>
|
||||
|
||||
<p>Visit the <code>/knowledge</code> page to view your content organized by different sections. This page also
|
||||
provides a visual demonstration of the graph database structure, enhancing your understanding of content
|
||||
relationships.</p>
|
||||
|
||||
<p>This streamlined setup ensures intuitive onboarding while offering robust customization options. Whether you are
|
||||
a novice or an advanced user, Minne is designed to deliver a smooth experience and reliable performance.</p>
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends "documentation/base.html" %}
|
||||
{% block article %}
|
||||
<h1 class="text-3xl mb-2">Documentation</h1>
|
||||
<p>
|
||||
Personalised Knowledge Management (PKM) is a system designed to help individuals organise, store, and retrieve
|
||||
information effectively. It empowers users to create a personalised workflow for managing knowledge, enabling
|
||||
better decision-making and productivity.
|
||||
</p>
|
||||
<p>
|
||||
This documentation will guide you through the core concepts, tools, and best practices for building and
|
||||
maintaining your own PKM system.
|
||||
</p>
|
||||
<div class="card bg-base-200 rounded-lg shadow-md">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title not-prose">Getting Started</h3>
|
||||
<p>
|
||||
To begin, explore the sections in the navigation menu. Each section provides detailed insights into
|
||||
different
|
||||
aspects of PKM, from foundational principles to advanced techniques.
|
||||
</p>
|
||||
<div class="card-actions">
|
||||
<a href="/documentation/quick-start" class="btn btn-primary text-primary-content" hx-boost="true">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,28 +0,0 @@
|
||||
<ul class="menu bg-base-200 rounded-box w-full ">
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/index' }}" href="/documentation">Start</a></li>
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/get-started' }}"
|
||||
href="/documentation/get-started">Get Started</a></li>
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/mobile-friendly' }}"
|
||||
href="/documentation/mobile-friendly">Mobile friendly</a></li>
|
||||
<li><a hx-boost="true" class="{{'menu-active' if current_path=='/privacy-policy' }}"
|
||||
href="/documentation/privacy-policy">Privacy Policy</a></li>
|
||||
<li>
|
||||
<details open>
|
||||
<summary>Core Concepts</summary>
|
||||
<ul>
|
||||
<li><a hx-boost="true" href="/documentation/submenu1">What is PKM?</a></li>
|
||||
<li><a hx-boost="true" href="/documentation/submenu2">Benefits of PKM</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary>Tools & Techniques</summary>
|
||||
<ul>
|
||||
<li><a hx-boost="true" href="/documentation/tools">Tools for PKM</a></li>
|
||||
<li><a hx-boost="true" href="/documentation/techniques">Effective Techniques</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li><a hx-boost="true" href="/documentation/faq">FAQ</a></li>
|
||||
</ul>
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends 'documentation/base.html' %}
|
||||
{% block article %}
|
||||
<h1>Mobile Friendly Ingression: How to Submit Content from iOS to Minne</h1>
|
||||
<p>Minne is built with simplicity in mind. Whether you wish to save a file, capture a thought, or share a page,
|
||||
submitting content is effortless. Our server provides API access that enables users to perform actions using a
|
||||
personalized API key.</p>
|
||||
|
||||
<p>An iOS shortcut has been developed to streamline the process of sending content. To begin, navigate to
|
||||
<code>/account</code> and generate an API key. Once created, you will see an option to download the iOS shortcut.
|
||||
</p>
|
||||
|
||||
<p>After downloading the shortcut, update the "Get response from URL" authentication headers with your API key. If
|
||||
you are self-hosting, ensure the URL is adjusted accordingly.</p>
|
||||
|
||||
<p>The shortcut integrates seamlessly with iOS. When you "share with Minne," you will be prompted to provide
|
||||
instructions to the AI and to either choose an existing category or create a new one for your submission.</p>
|
||||
|
||||
<p>While an Android solution is in the works, for now you can add the web app to your home screen as a Progressive
|
||||
Web App (PWA) for a similar mobile-friendly experience.</p>
|
||||
{% endblock %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% extends 'documentation/base.html' %}
|
||||
{% block article %}
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>We value your privacy and are committed to protecting your personal information. This policy
|
||||
outlines how we handle your data and ensures transparency in our practices.</p>
|
||||
|
||||
<h2>Data Collection</h2>
|
||||
<p>We only collect data that is necessary for the functionality of our services. Any data you upload to
|
||||
our site remains your property and will not be shared with third parties unless required by law.</p>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<p>We do not use cookies for tracking or analytics. The cookies we employ are strictly for session
|
||||
management, ensuring a smooth and secure user experience.</p>
|
||||
|
||||
<h2>No Unnecessary Data Extraction</h2>
|
||||
<p>We believe that unnecessary data extraction is unethical and a poor practice. We only collect the
|
||||
minimum amount of data required to provide our services effectively, ensuring your privacy is respected at all
|
||||
times.</p>
|
||||
|
||||
<h2>Your Rights</h2>
|
||||
<p>You have the right to access, modify, or delete your data at any time. If you have any concerns
|
||||
about how your data is handled, please contact us.</p>
|
||||
|
||||
<h2>Changes to This Policy</h2>
|
||||
<p>We may update this privacy policy from time to time. Any changes will be posted on this page, and we
|
||||
encourage you to review it periodically.</p>
|
||||
{% endblock %}
|
||||
@@ -21,8 +21,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Get config
|
||||
let config = get_config()?;
|
||||
|
||||
// Set up server components
|
||||
let html_state = HtmlState::new(&config).await?;
|
||||
// Set up router states
|
||||
let db = Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
// Ensure db is initialized
|
||||
db.ensure_initialized().await?;
|
||||
|
||||
let session_store = Arc::new(db.create_session_store().await?);
|
||||
let openai_client = Arc::new(async_openai::Client::new());
|
||||
|
||||
let html_state = HtmlState::new_with_resources(db, openai_client, session_store)?;
|
||||
|
||||
let api_state = ApiState {
|
||||
db: html_state.db.clone(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use api_router::{api_routes_v1, api_state::ApiState};
|
||||
use axum::{extract::FromRef, Router};
|
||||
use common::utils::config::get_config;
|
||||
use common::{storage::db::SurrealDbClient, utils::config::get_config};
|
||||
use html_router::{html_routes, html_state::HtmlState};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
@@ -18,7 +20,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = get_config()?;
|
||||
|
||||
// Set up router states
|
||||
let html_state = HtmlState::new(&config).await?;
|
||||
let db = Arc::new(
|
||||
SurrealDbClient::new(
|
||||
&config.surrealdb_address,
|
||||
&config.surrealdb_username,
|
||||
&config.surrealdb_password,
|
||||
&config.surrealdb_namespace,
|
||||
&config.surrealdb_database,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
// Ensure db is initialized
|
||||
db.ensure_initialized().await?;
|
||||
|
||||
let session_store = Arc::new(db.create_session_store().await?);
|
||||
let openai_client = Arc::new(async_openai::Client::new());
|
||||
|
||||
let html_state = HtmlState::new_with_resources(db, openai_client, session_store)?;
|
||||
|
||||
let api_state = ApiState {
|
||||
db: html_state.db.clone(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user