refactor: additional responsibilities to middleware, simplified handlers

fix
This commit is contained in:
Per Stark
2026-01-17 21:04:27 +01:00
parent a9fda67209
commit ece744d5a0
16 changed files with 286 additions and 293 deletions

View File

@@ -25,6 +25,56 @@ pub struct CategoryResponse {
category: String,
}
use std::str::FromStr;
/// Supported UI themes.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum Theme {
Light,
Dark,
WarmPaper,
ObsidianPrism,
#[default]
System,
}
impl FromStr for Theme {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"light" => Ok(Self::Light),
"dark" => Ok(Self::Dark),
"warm-paper" => Ok(Self::WarmPaper),
"obsidian-prism" => Ok(Self::ObsidianPrism),
"system" => Ok(Self::System),
_ => Err(()),
}
}
}
impl Theme {
pub fn as_str(&self) -> &'static str {
match self {
Self::Light => "light",
Self::Dark => "dark",
Self::WarmPaper => "warm-paper",
Self::ObsidianPrism => "obsidian-prism",
Self::System => "system",
}
}
/// Returns the theme that should be initially applied.
/// For "system", defaults to "light".
pub fn initial_theme(&self) -> &'static str {
match self {
Self::System => "light",
other => other.as_str(),
}
}
}
stored_object!(
#[allow(clippy::unsafe_derive_deserialize)]
User, "user", {
@@ -36,7 +86,7 @@ stored_object!(
#[serde(default)]
timezone: String,
#[serde(default)]
theme: String
theme: Theme
});
#[async_trait]
@@ -73,11 +123,8 @@ fn validate_timezone(input: &str) -> String {
}
/// Ensures a theme string is valid, defaulting to "system" when invalid.
fn validate_theme(input: &str) -> String {
match input {
"light" | "dark" | "system" => input.to_owned(),
_ => "system".to_owned(),
}
fn validate_theme(input: &str) -> Theme {
Theme::from_str(input).unwrap_or_default()
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -212,7 +259,7 @@ impl User {
.bind(("created_at", surrealdb::Datetime::from(now)))
.bind(("updated_at", surrealdb::Datetime::from(now)))
.bind(("timezone", validated_tz))
.bind(("theme", validated_theme))
.bind(("theme", validated_theme.as_str()))
.await?
.take(1)?;
@@ -490,7 +537,7 @@ impl User {
let validated_theme = validate_theme(theme);
db.query("UPDATE type::thing('user', $user_id) SET theme = $theme")
.bind(("user_id", user_id.to_string()))
.bind(("theme", validated_theme))
.bind(("theme", validated_theme.as_str()))
.await?;
Ok(())
}
@@ -1152,10 +1199,10 @@ mod tests {
#[tokio::test]
async fn test_validate_theme() {
assert_eq!(validate_theme("light"), "light");
assert_eq!(validate_theme("dark"), "dark");
assert_eq!(validate_theme("system"), "system");
assert_eq!(validate_theme("invalid"), "system");
assert_eq!(validate_theme("light"), Theme::Light);
assert_eq!(validate_theme("dark"), Theme::Dark);
assert_eq!(validate_theme("system"), Theme::System);
assert_eq!(validate_theme("invalid"), Theme::System);
}
#[tokio::test]
@@ -1172,7 +1219,7 @@ mod tests {
.await
.expect("Failed to create user");
assert_eq!(user.theme, "system");
assert_eq!(user.theme, Theme::System);
User::update_theme(&user.id, "dark", &db)
.await
@@ -1183,7 +1230,7 @@ mod tests {
.await
.expect("get user")
.unwrap();
assert_eq!(updated.theme, "dark");
assert_eq!(updated.theme, Theme::Dark);
// Invalid theme should default to system (but update_theme calls validate_theme)
User::update_theme(&user.id, "invalid", &db)
@@ -1194,6 +1241,6 @@ mod tests {
.await
.expect("get user")
.unwrap();
assert_eq!(updated2.theme, "system");
assert_eq!(updated2.theme, Theme::System);
}
}

View File

@@ -2,7 +2,11 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use common::storage::{db::SurrealDbClient, types::user::User, types::StoredObject};
use common::storage::{
db::SurrealDbClient,
types::user::{Theme, User},
types::StoredObject,
};
use serde::Deserialize;
use tracing::{info, warn};
@@ -212,7 +216,7 @@ pub(crate) async fn ensure_eval_user(db: &SurrealDbClient) -> Result<User> {
api_key: None,
admin: false,
timezone: "UTC".to_string(),
theme: "system".to_string(),
theme: Theme::System,
};
if let Some(existing) = db.get_item::<User>(user.get_id()).await? {

View File

@@ -97,11 +97,6 @@
--border: 2px;
}
/* ==========================================================================
THEME: Obsidian Prism
A forward-looking neobrutalist dark theme. Cool obsidian base,
prismatic violet shadows, dual-accent system (Signal + Ember).
========================================================================== */
[data-theme="obsidian-prism"] {
color-scheme: dark;
@@ -151,6 +146,54 @@
--nb-shadow-hover: 6px 6px 0 0 oklch(6% 0.08 calc(var(--nb-shadow-hue) + 15));
}
[data-theme="warm-paper"] {
color-scheme: light;
/* --- Canvas & Surfaces: Warm cream paper (more yellow than light) --- */
--color-base-100: oklch(97% 0.025 85);
--color-base-200: oklch(93% 0.028 83);
--color-base-300: oklch(88% 0.032 80);
--color-base-content: oklch(18% 0.015 75);
/* --- Primary: Warm Amber/Gold (the landing page CTA color) --- */
--color-primary: oklch(72% 0.16 75);
--color-primary-content: oklch(18% 0.02 75);
/* --- Secondary: Warm Terracotta --- */
--color-secondary: oklch(55% 0.14 45);
--color-secondary-content: oklch(98% 0.01 85);
/* --- Accent: Deep Charcoal (for contrast buttons like "View on GitHub") --- */
--color-accent: oklch(22% 0.01 80);
--color-accent-content: oklch(98% 0.02 85);
/* --- Neutral: Warm Charcoal --- */
--color-neutral: oklch(20% 0.015 75);
--color-neutral-content: oklch(96% 0.015 85);
/* --- Semantic Colors (warmer variants) --- */
--color-info: oklch(58% 0.12 230);
--color-info-content: oklch(98% 0.01 230);
--color-success: oklch(62% 0.15 155);
--color-success-content: oklch(98% 0.01 155);
--color-warning: oklch(78% 0.16 70);
--color-warning-content: oklch(20% 0.04 70);
--color-error: oklch(58% 0.20 25);
--color-error-content: oklch(98% 0.02 25);
/* --- Radii (NB Law: Zero) --- */
--radius-selector: 0rem;
--radius-field: 0rem;
--radius-box: 0rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 2px;
/* --- Classic Black Shadow --- */
--nb-shadow: 4px 4px 0 0 #000;
--nb-shadow-hover: 6px 6px 0 0 #000;
}
body {
background-color: var(--color-base-100);
color: var(--color-base-content);
@@ -956,10 +999,6 @@
}
}
/* ==========================================================================
OBSIDIAN PRISM: Component Overrides & Delight Features
========================================================================== */
/* Prismatic shadow hue shift on hover */
[data-theme="obsidian-prism"] .nb-panel:hover,
[data-theme="obsidian-prism"] .nb-card:hover,
@@ -969,8 +1008,15 @@
/* Focus state: breathing shadow pulse */
@keyframes shadow-breathe {
0%, 100% { box-shadow: 6px 6px 0 0 oklch(8% 0.08 305); }
50% { box-shadow: 7px 7px 0 0 oklch(10% 0.10 310); }
0%,
100% {
box-shadow: 6px 6px 0 0 oklch(8% 0.08 305);
}
50% {
box-shadow: 7px 7px 0 0 oklch(10% 0.10 310);
}
}
[data-theme="obsidian-prism"] .nb-btn:focus-visible,

File diff suppressed because one or more lines are too long

View File

@@ -65,3 +65,9 @@ impl ProvidesTemplateEngine for HtmlState {
&self.templates
}
}
impl crate::middlewares::response_middleware::ProvidesHtmlState for HtmlState {
fn html_state(&self) -> &HtmlState {
self
}
}

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use axum::{
extract::{Request, State},
http::{HeaderName, StatusCode},
@@ -6,13 +8,24 @@ use axum::{
Extension,
};
use axum_htmx::{HxRequest, HX_TRIGGER};
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine};
use minijinja::{context, Value};
use common::{
error::AppError,
utils::template_engine::{ProvidesTemplateEngine, Value},
};
use minijinja::context;
use serde::Serialize;
use serde_json::json;
use tracing::error;
use crate::AuthSessionType;
use crate::{html_state::HtmlState, AuthSessionType};
use common::storage::types::{
conversation::Conversation,
user::{Theme, User},
};
pub trait ProvidesHtmlState {
fn html_state(&self) -> &HtmlState;
}
#[derive(Clone)]
pub enum TemplateKind {
@@ -106,8 +119,10 @@ struct ContextWrapper<'a> {
user_theme: &'a str,
initial_theme: &'a str,
is_authenticated: bool,
user: Option<&'a User>,
conversation_archive: Vec<Conversation>,
#[serde(flatten)]
context: &'a Value,
context: HashMap<String, Value>,
}
pub async fn with_template_response<S>(
@@ -117,25 +132,23 @@ pub async fn with_template_response<S>(
next: Next,
) -> Response
where
S: ProvidesTemplateEngine + Clone + Send + Sync + 'static,
S: ProvidesTemplateEngine + ProvidesHtmlState + Clone + Send + Sync + 'static,
{
// Determine theme context
let (user_theme, initial_theme, is_authenticated) =
let mut user_theme = Theme::System.as_str();
let mut initial_theme = Theme::System.initial_theme();
let mut is_authenticated = false;
let mut current_user_id = None;
{
if let Some(auth) = req.extensions().get::<AuthSessionType>() {
if let Some(user) = &auth.current_user {
let theme = user.theme.as_str();
// For explicit themes (not "system"), use the theme directly as initial_theme
let initial = match theme {
"system" => "light",
other => other, // "light", "dark", "obsidian-prism", etc.
};
(theme.to_string(), initial.to_string(), true)
} else {
("system".to_string(), "light".to_string(), false)
is_authenticated = true;
current_user_id = Some(user.id.clone());
user_theme = user.theme.as_str();
initial_theme = user.theme.initial_theme();
}
} else {
("system".to_string(), "light".to_string(), false)
};
}
}
let response = next.run(req).await;
@@ -145,6 +158,20 @@ where
if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() {
let template_engine = state.template_engine();
let mut current_user = None;
let mut conversation_archive = Vec::new();
if let Some(user_id) = current_user_id {
let html_state = state.html_state();
if let Ok(Some(user)) = html_state.db.get_item::<User>(&user_id).await {
// Fetch conversation archive globally for authenticated users
if let Ok(archive) = User::get_user_conversations(&user.id, &html_state.db).await {
conversation_archive = archive;
}
current_user = Some(user);
}
}
// Helper to forward relevant headers
fn forward_headers(from: &axum::http::HeaderMap, to: &mut axum::http::HeaderMap) {
for &header_name in HTMX_HEADERS_TO_FORWARD {
@@ -156,11 +183,28 @@ where
}
}
// Convert minijinja::Value to HashMap if it's a map, otherwise use empty HashMap
let context_map = if template_response.context.kind() == minijinja::value::ValueKind::Map {
let mut map = HashMap::new();
if let Ok(keys) = template_response.context.try_iter() {
for key in keys {
if let Ok(val) = template_response.context.get_item(&key) {
map.insert(key.to_string(), val);
}
}
}
map
} else {
HashMap::new()
};
let context = ContextWrapper {
user_theme: &user_theme,
initial_theme: &initial_theme,
is_authenticated,
context: &template_response.context,
user: current_user.as_ref(),
conversation_archive,
context: context_map,
};
match &template_response.template_kind {

View File

@@ -9,40 +9,36 @@ use crate::{
},
AuthSessionType,
};
use common::storage::types::{conversation::Conversation, user::User};
use common::storage::types::user::{Theme, User};
use crate::html_state::HtmlState;
#[derive(Serialize)]
pub struct AccountPageData {
user: User,
timezones: Vec<String>,
conversation_archive: Vec<Conversation>,
theme_options: Vec<String>,
}
pub async fn show_account_page(
RequireUser(user): RequireUser,
State(state): State<HtmlState>,
RequireUser(_user): RequireUser,
State(_state): State<HtmlState>,
) -> Result<impl IntoResponse, HtmlError> {
let timezones = TZ_VARIANTS
.iter()
.map(std::string::ToString::to_string)
.collect();
let theme_options = vec![
"light".to_string(),
"dark".to_string(),
"obsidian-prism".to_string(),
"system".to_string(),
Theme::Light.as_str().to_string(),
Theme::Dark.as_str().to_string(),
Theme::WarmPaper.as_str().to_string(),
Theme::ObsidianPrism.as_str().to_string(),
Theme::System.as_str().to_string(),
];
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"auth/account_settings.html",
AccountPageData {
user,
timezones,
conversation_archive,
theme_options,
},
))
@@ -54,25 +50,17 @@ pub async fn set_api_key(
auth: AuthSessionType,
) -> Result<impl IntoResponse, HtmlError> {
// Generate and set the API key
let api_key = User::set_api_key(&user.id, &state.db).await?;
User::set_api_key(&user.id, &state.db).await?;
// Clear the cache so new requests have access to the user with api key
auth.cache_clear_user(user.id.to_string());
// Update the user's API key
let updated_user = User {
api_key: Some(api_key),
..user.clone()
};
// Render the API key section block
Ok(TemplateResponse::new_partial(
"auth/account_settings.html",
"api_key_section",
AccountPageData {
user: updated_user,
timezones: vec![],
conversation_archive: vec![],
theme_options: vec![],
},
))
@@ -108,12 +96,6 @@ pub async fn update_timezone(
// Clear the cache
auth.cache_clear_user(user.id.to_string());
// Update the user's API key
let updated_user = User {
timezone: form.timezone,
..user.clone()
};
let timezones = TZ_VARIANTS
.iter()
.map(std::string::ToString::to_string)
@@ -124,9 +106,7 @@ pub async fn update_timezone(
"auth/account_settings.html",
"timezone_section",
AccountPageData {
user: updated_user,
timezones,
conversation_archive: vec![],
theme_options: vec![],
},
))
@@ -148,26 +128,19 @@ pub async fn update_theme(
// Clear the cache
auth.cache_clear_user(user.id.to_string());
// Update the user's theme
let updated_user = User {
theme: form.theme,
..user.clone()
};
let theme_options = vec![
"light".to_string(),
"dark".to_string(),
"obsidian-prism".to_string(),
"system".to_string(),
Theme::Light.as_str().to_string(),
Theme::Dark.as_str().to_string(),
Theme::WarmPaper.as_str().to_string(),
Theme::ObsidianPrism.as_str().to_string(),
Theme::System.as_str().to_string(),
];
Ok(TemplateResponse::new_partial(
"auth/account_settings.html",
"theme_section",
AccountPageData {
user: updated_user,
timezones: vec![],
conversation_archive: vec![],
theme_options,
},
))

View File

@@ -10,7 +10,6 @@ use common::{
error::AppError,
storage::types::{
analytics::Analytics,
conversation::Conversation,
knowledge_entity::KnowledgeEntity,
system_prompts::{
DEFAULT_IMAGE_PROCESSING_PROMPT, DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT,
@@ -18,7 +17,6 @@ use common::{
},
system_settings::SystemSettings,
text_chunk::TextChunk,
user::User,
},
};
use tracing::{error, info};
@@ -33,13 +31,11 @@ use crate::{
#[derive(Serialize)]
pub struct AdminPanelData {
user: User,
settings: SystemSettings,
analytics: Option<Analytics>,
users: Option<i64>,
default_query_prompt: String,
default_image_prompt: String,
conversation_archive: Vec<Conversation>,
available_models: Option<ListModelResponse>,
current_section: AdminSection,
}
@@ -64,7 +60,7 @@ pub struct AdminPanelQuery {
pub async fn show_admin_panel(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
RequireUser(_user): RequireUser,
Query(query): Query<AdminPanelQuery>,
) -> Result<impl IntoResponse, HtmlError> {
let section = match query.section.as_deref() {
@@ -72,10 +68,7 @@ pub async fn show_admin_panel(
_ => AdminSection::Overview,
};
let (settings, conversation_archive) = tokio::try_join!(
SystemSettings::get_current(&state.db),
User::get_user_conversations(&user.id, &state.db)
)?;
let settings = SystemSettings::get_current(&state.db).await?;
let (analytics, users) = if section == AdminSection::Overview {
let (analytics, users) = tokio::try_join!(
@@ -103,14 +96,12 @@ pub async fn show_admin_panel(
Ok(TemplateResponse::new_template(
"admin/base.html",
AdminPanelData {
user,
settings,
analytics,
available_models,
users,
default_query_prompt: DEFAULT_QUERY_SYSTEM_PROMPT.to_string(),
default_image_prompt: DEFAULT_IMAGE_PROCESSING_PROMPT.to_string(),
conversation_archive,
current_section: section,
},
))

View File

@@ -6,7 +6,7 @@ use axum::{
use axum_htmx::HxBoosted;
use serde::{Deserialize, Serialize};
use common::storage::types::user::User;
use common::storage::types::user::{Theme, User};
use crate::{
html_state::HtmlState,
@@ -50,7 +50,7 @@ pub async fn process_signup_and_show_verification(
form.password,
&state.db,
form.timezone,
"system".to_string(),
Theme::System.as_str().to_string(),
)
.await
{

View File

@@ -45,10 +45,8 @@ where
#[derive(Serialize)]
pub struct ChatPageData {
user: User,
history: Vec<Message>,
conversation: Option<Conversation>,
conversation_archive: Vec<Conversation>,
}
pub async fn show_initialized_chat(
@@ -76,16 +74,12 @@ pub async fn show_initialized_chat(
state.db.store_item(ai_message.clone()).await?;
state.db.store_item(user_message.clone()).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let messages = vec![user_message, ai_message];
let mut response = TemplateResponse::new_template(
"chat/base.html",
ChatPageData {
history: messages,
user,
conversation_archive,
conversation: Some(conversation.clone()),
},
)
@@ -100,17 +94,13 @@ pub async fn show_initialized_chat(
}
pub async fn show_chat_base(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
State(_state): State<HtmlState>,
RequireUser(_user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> {
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"chat/base.html",
ChatPageData {
history: vec![],
user,
conversation_archive,
conversation: None,
},
))
@@ -126,8 +116,6 @@ pub async fn show_existing_chat(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> {
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let (conversation, messages) =
Conversation::get_complete_conversation(conversation_id.as_str(), &user.id, &state.db)
.await?;
@@ -136,9 +124,7 @@ pub async fn show_existing_chat(
"chat/base.html",
ChatPageData {
history: messages,
user,
conversation: Some(conversation),
conversation_archive,
},
))
}
@@ -232,8 +218,6 @@ pub struct PatchConversationTitle {
#[derive(Serialize)]
pub struct DrawerContext {
user: User,
conversation_archive: Vec<Conversation>,
#[serde(skip_serializing_if = "Option::is_none")]
edit_conversation_id: Option<String>,
}
@@ -242,20 +226,19 @@ pub async fn show_conversation_editing_title(
RequireUser(user): RequireUser,
Path(conversation_id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let conversation: Conversation = state
.db
.get_item(&conversation_id)
.await?
.ok_or_else(|| AppError::NotFound("Conversation not found".to_string()))?;
let owns = conversation_archive
.iter()
.any(|c| c.id == conversation_id && c.user_id == user.id);
if !owns {
if conversation.user_id != user.id {
return Ok(TemplateResponse::unauthorized().into_response());
}
Ok(TemplateResponse::new_template(
"sidebar.html",
DrawerContext {
user,
conversation_archive,
edit_conversation_id: Some(conversation_id),
},
)
@@ -270,13 +253,9 @@ pub async fn patch_conversation_title(
) -> Result<impl IntoResponse, HtmlError> {
Conversation::patch_title(&conversation_id, &user.id, &form.title, &state.db).await?;
let updated_conversations = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"sidebar.html",
DrawerContext {
user,
conversation_archive: updated_conversations,
edit_conversation_id: None,
},
)
@@ -303,29 +282,21 @@ pub async fn delete_conversation(
.delete_item::<Conversation>(&conversation_id)
.await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"sidebar.html",
DrawerContext {
user,
conversation_archive,
edit_conversation_id: None,
},
)
.into_response())
}
pub async fn reload_sidebar(
State(state): State<HtmlState>,
RequireUser(user): RequireUser,
State(_state): State<HtmlState>,
RequireUser(_user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> {
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"sidebar.html",
DrawerContext {
user,
conversation_archive,
edit_conversation_id: None,
},
)

View File

@@ -7,8 +7,8 @@ use axum_htmx::{HxBoosted, HxRequest, HxTarget};
use serde::{Deserialize, Serialize};
use common::storage::types::{
conversation::Conversation, file_info::FileInfo, knowledge_entity::KnowledgeEntity,
text_chunk::TextChunk, text_content::TextContent, user::User,
file_info::FileInfo, knowledge_entity::KnowledgeEntity, text_chunk::TextChunk,
text_content::TextContent, user::User,
};
use crate::{
@@ -26,18 +26,15 @@ const CONTENTS_PER_PAGE: usize = 12;
#[derive(Serialize)]
pub struct ContentPageData {
user: User,
text_contents: Vec<TextContent>,
categories: Vec<String>,
selected_category: Option<String>,
conversation_archive: Vec<Conversation>,
pagination: Pagination,
page_query: String,
}
#[derive(Serialize)]
pub struct RecentTextContentData {
pub user: User,
pub text_contents: Vec<TextContent>,
}
@@ -81,13 +78,10 @@ pub async fn show_content_page(
})
.unwrap_or_default();
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let data = ContentPageData {
user,
text_contents,
categories,
selected_category: params.category.clone(),
conversation_archive,
pagination,
page_query,
};
@@ -112,13 +106,12 @@ pub async fn show_text_content_edit_form(
#[derive(Serialize)]
pub struct TextContentEditModal {
pub user: User,
pub text_content: TextContent,
}
Ok(TemplateResponse::new_template(
"content/edit_text_content_modal.html",
TextContentEditModal { user, text_content },
TextContentEditModal { text_content },
))
}
@@ -145,10 +138,7 @@ pub async fn patch_text_content(
return Ok(TemplateResponse::new_template(
"dashboard/recent_content.html",
RecentTextContentData {
user,
text_contents,
},
RecentTextContentData { text_contents },
));
}
@@ -159,17 +149,14 @@ pub async fn patch_text_content(
);
let text_contents = truncate_text_contents(page_contents);
let categories = User::get_user_categories(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_partial(
"content/base.html",
"main",
ContentPageData {
user,
text_contents,
categories,
selected_category: None,
conversation_archive,
pagination,
page_query: String::new(),
},
@@ -209,16 +196,13 @@ pub async fn delete_text_content(
);
let text_contents = truncate_text_contents(page_contents);
let categories = User::get_user_categories(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"content/content_list.html",
ContentPageData {
user,
text_contents,
categories,
selected_category: None,
conversation_archive,
pagination,
page_query: String::new(),
},
@@ -234,13 +218,12 @@ pub async fn show_content_read_modal(
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
#[derive(Serialize)]
pub struct TextContentReadModalData {
pub user: User,
pub text_content: TextContent,
}
Ok(TemplateResponse::new_template(
"content/read_content_modal.html",
TextContentReadModalData { user, text_content },
TextContentReadModalData { text_content },
))
}
@@ -253,9 +236,6 @@ pub async fn show_recent_content(
Ok(TemplateResponse::new_template(
"dashboard/recent_content.html",
RecentTextContentData {
user,
text_contents,
},
RecentTextContentData { text_contents },
))
}

View File

@@ -21,19 +21,17 @@ use common::storage::types::user::DashboardStats;
use common::{
error::AppError,
storage::types::{
conversation::Conversation, file_info::FileInfo, ingestion_task::IngestionTask,
knowledge_entity::KnowledgeEntity, knowledge_relationship::KnowledgeRelationship,
text_chunk::TextChunk, text_content::TextContent, user::User,
file_info::FileInfo, ingestion_task::IngestionTask, knowledge_entity::KnowledgeEntity,
knowledge_relationship::KnowledgeRelationship, text_chunk::TextChunk,
text_content::TextContent, user::User,
},
};
#[derive(Serialize)]
pub struct IndexPageData {
user: Option<User>,
text_contents: Vec<TextContent>,
stats: DashboardStats,
active_jobs: Vec<IngestionTask>,
conversation_archive: Vec<Conversation>,
}
pub async fn index_handler(
@@ -44,7 +42,7 @@ pub async fn index_handler(
return Ok(TemplateResponse::redirect("/signin"));
};
let (text_contents, conversation_archive, stats, active_jobs) = try_join!(
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),
@@ -56,10 +54,8 @@ pub async fn index_handler(
Ok(TemplateResponse::new_template(
"dashboard/base.html",
IndexPageData {
user: Some(user),
text_contents,
stats,
conversation_archive,
active_jobs,
},
))
@@ -68,7 +64,6 @@ pub async fn index_handler(
#[derive(Serialize)]
pub struct LatestTextContentData {
text_contents: Vec<TextContent>,
user: User,
}
pub async fn delete_text_content(
@@ -105,10 +100,7 @@ pub async fn delete_text_content(
Ok(TemplateResponse::new_partial(
"dashboard/recent_content.html",
"latest_content_section",
LatestTextContentData {
user: user.clone(),
text_contents,
},
LatestTextContentData { text_contents },
))
}
@@ -136,7 +128,6 @@ async fn get_and_validate_text_content(
#[derive(Serialize)]
pub struct ActiveJobsData {
pub active_jobs: Vec<IngestionTask>,
pub user: User,
}
#[derive(Serialize)]
@@ -161,7 +152,6 @@ struct TaskArchiveEntry {
#[derive(Serialize)]
struct TaskArchiveData {
user: User,
tasks: Vec<TaskArchiveEntry>,
}
@@ -177,10 +167,7 @@ pub async fn delete_job(
Ok(TemplateResponse::new_partial(
"dashboard/active_jobs.html",
"active_jobs_section",
ActiveJobsData {
user: user.clone(),
active_jobs,
},
ActiveJobsData { active_jobs },
))
}
@@ -192,10 +179,7 @@ pub async fn show_active_jobs(
Ok(TemplateResponse::new_template(
"dashboard/active_jobs.html",
ActiveJobsData {
user: user.clone(),
active_jobs,
},
ActiveJobsData { active_jobs },
))
}
@@ -233,10 +217,7 @@ pub async fn show_task_archive(
Ok(TemplateResponse::new_template(
"dashboard/task_archive_modal.html",
TaskArchiveData {
user,
tasks: entries,
},
TaskArchiveData { tasks: entries },
))
}

View File

@@ -120,13 +120,12 @@ pub async fn process_ingress_form(
#[derive(Serialize)]
struct NewTasksData {
user: User,
tasks: Vec<IngestionTask>,
}
Ok(TemplateResponse::new_template(
"dashboard/current_task.html",
NewTasksData { user, tasks },
NewTasksData { tasks },
))
}

View File

@@ -17,7 +17,6 @@ use serde::{
use common::{
error::AppError,
storage::types::{
conversation::Conversation,
knowledge_entity::{KnowledgeEntity, KnowledgeEntityType},
knowledge_relationship::KnowledgeRelationship,
user::User,
@@ -333,12 +332,10 @@ pub struct KnowledgeBaseData {
entities: Vec<KnowledgeEntity>,
visible_entities: Vec<KnowledgeEntity>,
relationships: Vec<RelationshipTableRow>,
user: User,
entity_types: Vec<String>,
content_categories: Vec<String>,
selected_entity_type: Option<String>,
selected_content_category: Option<String>,
conversation_archive: Vec<Conversation>,
pagination: Pagination,
page_query: String,
relationship_type_options: Vec<String>,
@@ -481,18 +478,15 @@ async fn build_knowledge_base_data(
relationship_type_options,
default_relationship_type,
} = build_relationship_table_data(entities.clone(), filtered_relationships);
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(KnowledgeBaseData {
entities,
visible_entities,
relationships,
user: user.clone(),
entity_types,
content_categories,
selected_entity_type: params.entity_type.clone(),
selected_content_category: params.content_category.clone(),
conversation_archive,
pagination,
page_query,
relationship_type_options,
@@ -861,7 +855,6 @@ pub async fn show_edit_knowledge_entity_form(
pub struct EntityData {
entity: KnowledgeEntity,
entity_types: Vec<String>,
user: User,
}
// Get entity types
@@ -878,7 +871,6 @@ pub async fn show_edit_knowledge_entity_form(
EntityData {
entity,
entity_types,
user,
},
))
}
@@ -895,7 +887,6 @@ pub struct PatchKnowledgeEntityParams {
pub struct EntityListData {
visible_entities: Vec<KnowledgeEntity>,
pagination: Pagination,
user: User,
entity_types: Vec<String>,
content_categories: Vec<String>,
selected_entity_type: Option<String>,
@@ -943,7 +934,6 @@ pub async fn patch_knowledge_entity(
EntityListData {
visible_entities,
pagination,
user,
entity_types,
content_categories,
selected_entity_type: None,
@@ -982,7 +972,6 @@ pub async fn delete_knowledge_entity(
EntityListData {
visible_entities,
pagination,
user,
entity_types,
content_categories,
selected_entity_type: None,

View File

@@ -14,16 +14,13 @@ use crate::middlewares::{
response_middleware::{HtmlError, TemplateResponse},
};
use common::storage::types::{
conversation::Conversation, ingestion_payload::IngestionPayload, ingestion_task::IngestionTask,
scratchpad::Scratchpad, user::User,
ingestion_payload::IngestionPayload, ingestion_task::IngestionTask, scratchpad::Scratchpad,
};
#[derive(Serialize)]
pub struct ScratchpadPageData {
user: User,
scratchpads: Vec<ScratchpadListItem>,
archived_scratchpads: Vec<ScratchpadArchiveItem>,
conversation_archive: Vec<Conversation>,
#[serde(skip_serializing_if = "Option::is_none")]
new_scratchpad: Option<ScratchpadDetail>,
}
@@ -38,9 +35,7 @@ pub struct ScratchpadListItem {
#[derive(Serialize)]
pub struct ScratchpadDetailData {
user: User,
scratchpad: ScratchpadDetail,
conversation_archive: Vec<Conversation>,
is_editing_title: bool,
}
@@ -135,7 +130,6 @@ pub async fn show_scratchpad_page(
) -> Result<impl IntoResponse, HtmlError> {
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -149,10 +143,8 @@ pub async fn show_scratchpad_page(
"scratchpad/base.html",
"main",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None,
},
))
@@ -160,10 +152,8 @@ pub async fn show_scratchpad_page(
Ok(TemplateResponse::new_template(
"scratchpad/base.html",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None,
},
))
@@ -177,7 +167,6 @@ pub async fn show_scratchpad_modal(
Query(query): Query<EditTitleQuery>,
) -> Result<impl IntoResponse, HtmlError> {
let scratchpad = Scratchpad::get_by_id(&scratchpad_id, &user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let scratchpad_detail = ScratchpadDetail::from(&scratchpad);
@@ -187,9 +176,7 @@ pub async fn show_scratchpad_modal(
Ok(TemplateResponse::new_template(
"scratchpad/editor_modal.html",
ScratchpadDetailData {
user,
scratchpad: scratchpad_detail,
conversation_archive,
is_editing_title,
},
))
@@ -206,7 +193,6 @@ pub async fn create_scratchpad(
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -219,10 +205,8 @@ pub async fn create_scratchpad(
"scratchpad/base.html",
"main",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: Some(ScratchpadDetail::from(&scratchpad)),
},
))
@@ -257,14 +241,11 @@ pub async fn update_scratchpad_title(
Scratchpad::update_title(&scratchpad_id, &user.id, &form.title, &state.db).await?;
let scratchpad = Scratchpad::get_by_id(&scratchpad_id, &user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"scratchpad/editor_modal.html",
ScratchpadDetailData {
user,
scratchpad: ScratchpadDetail::from(&scratchpad),
conversation_archive,
is_editing_title: false,
},
))
@@ -279,7 +260,6 @@ pub async fn delete_scratchpad(
// Return the updated main section content
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
@@ -293,10 +273,8 @@ pub async fn delete_scratchpad(
"scratchpad/base.html",
"main",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None,
},
))
@@ -350,7 +328,6 @@ pub async fn ingest_scratchpad(
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -374,10 +351,8 @@ pub async fn ingest_scratchpad(
"scratchpad/base.html",
"main",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None,
},
);
@@ -399,7 +374,6 @@ pub async fn archive_scratchpad(
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -411,15 +385,59 @@ pub async fn archive_scratchpad(
Ok(TemplateResponse::new_template(
"scratchpad/base.html",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None,
},
))
}
pub async fn restore_scratchpad(
RequireUser(user): RequireUser,
State(state): State<HtmlState>,
Path(scratchpad_id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
Scratchpad::restore(&scratchpad_id, &user.id, &state.db).await?;
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect();
let archived_list: Vec<ScratchpadArchiveItem> = archived_scratchpads
.iter()
.map(ScratchpadArchiveItem::from)
.collect();
let trigger_payload = serde_json::json!({
"toast": {
"title": "Scratchpad restored",
"description": "The scratchpad is back in your active list.",
"type": "info"
}
});
let trigger_value = serde_json::to_string(&trigger_payload).unwrap_or_else(|_| {
r#"{"toast":{"title":"Scratchpad restored","description":"The scratchpad is back in your active list.","type":"info"}}"#.to_string()
});
let template_response = TemplateResponse::new_partial(
"scratchpad/base.html",
"main",
ScratchpadPageData {
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
new_scratchpad: None,
},
);
let mut response = template_response.into_response();
if let Ok(header_value) = HeaderValue::from_str(&trigger_value) {
response.headers_mut().insert(HX_TRIGGER, header_value);
}
Ok(response)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -509,52 +527,3 @@ mod tests {
assert_eq!(archive_item.ingested_at, None);
}
}
pub async fn restore_scratchpad(
RequireUser(user): RequireUser,
State(state): State<HtmlState>,
Path(scratchpad_id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
Scratchpad::restore(&scratchpad_id, &user.id, &state.db).await?;
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?;
let archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect();
let archived_list: Vec<ScratchpadArchiveItem> = archived_scratchpads
.iter()
.map(ScratchpadArchiveItem::from)
.collect();
let trigger_payload = serde_json::json!({
"toast": {
"title": "Scratchpad restored",
"description": "The scratchpad is back in your active list.",
"type": "info"
}
});
let trigger_value = serde_json::to_string(&trigger_payload).unwrap_or_else(|_| {
r#"{"toast":{"title":"Scratchpad restored","description":"The scratchpad is back in your active list.","type":"info"}}"#.to_string()
});
let template_response = TemplateResponse::new_partial(
"scratchpad/base.html",
"main",
ScratchpadPageData {
user,
scratchpads: scratchpad_list,
archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None,
},
);
let mut response = template_response.into_response();
if let Ok(header_value) = HeaderValue::from_str(&trigger_value) {
response.headers_mut().insert(HX_TRIGGER, header_value);
}
Ok(response)
}

View File

@@ -9,9 +9,7 @@ use axum::{
response::IntoResponse,
};
use common::storage::types::{
conversation::Conversation,
text_content::{deserialize_flexible_id, TextContent},
user::User,
StoredObject,
};
use retrieval_pipeline::{RetrievalConfig, SearchResult, SearchTarget, StrategyOutput};
@@ -194,10 +192,7 @@ pub async fn search_result_handler(
pub struct AnswerData {
search_result: Vec<SearchResultForTemplate>,
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
@@ -346,8 +341,6 @@ pub async fn search_result_handler(
AnswerData {
search_result: search_results_for_template,
query_param: final_query_param_for_template,
user,
conversation_archive,
},
))
}