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, 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!( stored_object!(
#[allow(clippy::unsafe_derive_deserialize)] #[allow(clippy::unsafe_derive_deserialize)]
User, "user", { User, "user", {
@@ -36,7 +86,7 @@ stored_object!(
#[serde(default)] #[serde(default)]
timezone: String, timezone: String,
#[serde(default)] #[serde(default)]
theme: String theme: Theme
}); });
#[async_trait] #[async_trait]
@@ -73,11 +123,8 @@ fn validate_timezone(input: &str) -> String {
} }
/// Ensures a theme string is valid, defaulting to "system" when invalid. /// Ensures a theme string is valid, defaulting to "system" when invalid.
fn validate_theme(input: &str) -> String { fn validate_theme(input: &str) -> Theme {
match input { Theme::from_str(input).unwrap_or_default()
"light" | "dark" | "system" => input.to_owned(),
_ => "system".to_owned(),
}
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@@ -212,7 +259,7 @@ impl User {
.bind(("created_at", surrealdb::Datetime::from(now))) .bind(("created_at", surrealdb::Datetime::from(now)))
.bind(("updated_at", surrealdb::Datetime::from(now))) .bind(("updated_at", surrealdb::Datetime::from(now)))
.bind(("timezone", validated_tz)) .bind(("timezone", validated_tz))
.bind(("theme", validated_theme)) .bind(("theme", validated_theme.as_str()))
.await? .await?
.take(1)?; .take(1)?;
@@ -490,7 +537,7 @@ impl User {
let validated_theme = validate_theme(theme); let validated_theme = validate_theme(theme);
db.query("UPDATE type::thing('user', $user_id) SET theme = $theme") db.query("UPDATE type::thing('user', $user_id) SET theme = $theme")
.bind(("user_id", user_id.to_string())) .bind(("user_id", user_id.to_string()))
.bind(("theme", validated_theme)) .bind(("theme", validated_theme.as_str()))
.await?; .await?;
Ok(()) Ok(())
} }
@@ -1152,10 +1199,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_validate_theme() { async fn test_validate_theme() {
assert_eq!(validate_theme("light"), "light"); assert_eq!(validate_theme("light"), Theme::Light);
assert_eq!(validate_theme("dark"), "dark"); assert_eq!(validate_theme("dark"), Theme::Dark);
assert_eq!(validate_theme("system"), "system"); assert_eq!(validate_theme("system"), Theme::System);
assert_eq!(validate_theme("invalid"), "system"); assert_eq!(validate_theme("invalid"), Theme::System);
} }
#[tokio::test] #[tokio::test]
@@ -1172,7 +1219,7 @@ mod tests {
.await .await
.expect("Failed to create user"); .expect("Failed to create user");
assert_eq!(user.theme, "system"); assert_eq!(user.theme, Theme::System);
User::update_theme(&user.id, "dark", &db) User::update_theme(&user.id, "dark", &db)
.await .await
@@ -1183,7 +1230,7 @@ mod tests {
.await .await
.expect("get user") .expect("get user")
.unwrap(); .unwrap();
assert_eq!(updated.theme, "dark"); assert_eq!(updated.theme, Theme::Dark);
// Invalid theme should default to system (but update_theme calls validate_theme) // Invalid theme should default to system (but update_theme calls validate_theme)
User::update_theme(&user.id, "invalid", &db) User::update_theme(&user.id, "invalid", &db)
@@ -1194,6 +1241,6 @@ mod tests {
.await .await
.expect("get user") .expect("get user")
.unwrap(); .unwrap();
assert_eq!(updated2.theme, "system"); assert_eq!(updated2.theme, Theme::System);
} }
} }

View File

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

View File

@@ -97,11 +97,6 @@
--border: 2px; --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"] { [data-theme="obsidian-prism"] {
color-scheme: dark; color-scheme: dark;
@@ -151,6 +146,54 @@
--nb-shadow-hover: 6px 6px 0 0 oklch(6% 0.08 calc(var(--nb-shadow-hue) + 15)); --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 { body {
background-color: var(--color-base-100); background-color: var(--color-base-100);
color: var(--color-base-content); color: var(--color-base-content);
@@ -956,10 +999,6 @@
} }
} }
/* ==========================================================================
OBSIDIAN PRISM: Component Overrides & Delight Features
========================================================================== */
/* Prismatic shadow hue shift on hover */ /* Prismatic shadow hue shift on hover */
[data-theme="obsidian-prism"] .nb-panel:hover, [data-theme="obsidian-prism"] .nb-panel:hover,
[data-theme="obsidian-prism"] .nb-card:hover, [data-theme="obsidian-prism"] .nb-card:hover,
@@ -969,8 +1008,15 @@
/* Focus state: breathing shadow pulse */ /* Focus state: breathing shadow pulse */
@keyframes shadow-breathe { @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, [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 &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::{ use axum::{
extract::{Request, State}, extract::{Request, State},
http::{HeaderName, StatusCode}, http::{HeaderName, StatusCode},
@@ -6,13 +8,24 @@ use axum::{
Extension, Extension,
}; };
use axum_htmx::{HxRequest, HX_TRIGGER}; use axum_htmx::{HxRequest, HX_TRIGGER};
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine}; use common::{
use minijinja::{context, Value}; error::AppError,
utils::template_engine::{ProvidesTemplateEngine, Value},
};
use minijinja::context;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use tracing::error; 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)] #[derive(Clone)]
pub enum TemplateKind { pub enum TemplateKind {
@@ -106,8 +119,10 @@ struct ContextWrapper<'a> {
user_theme: &'a str, user_theme: &'a str,
initial_theme: &'a str, initial_theme: &'a str,
is_authenticated: bool, is_authenticated: bool,
user: Option<&'a User>,
conversation_archive: Vec<Conversation>,
#[serde(flatten)] #[serde(flatten)]
context: &'a Value, context: HashMap<String, Value>,
} }
pub async fn with_template_response<S>( pub async fn with_template_response<S>(
@@ -117,25 +132,23 @@ pub async fn with_template_response<S>(
next: Next, next: Next,
) -> Response ) -> Response
where where
S: ProvidesTemplateEngine + Clone + Send + Sync + 'static, S: ProvidesTemplateEngine + ProvidesHtmlState + Clone + Send + Sync + 'static,
{ {
// Determine theme context let mut user_theme = Theme::System.as_str();
let (user_theme, initial_theme, is_authenticated) = 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(auth) = req.extensions().get::<AuthSessionType>() {
if let Some(user) = &auth.current_user { if let Some(user) = &auth.current_user {
let theme = user.theme.as_str(); is_authenticated = true;
// For explicit themes (not "system"), use the theme directly as initial_theme current_user_id = Some(user.id.clone());
let initial = match theme { user_theme = user.theme.as_str();
"system" => "light", initial_theme = user.theme.initial_theme();
other => other, // "light", "dark", "obsidian-prism", etc.
};
(theme.to_string(), initial.to_string(), true)
} else {
("system".to_string(), "light".to_string(), false)
} }
} else { }
("system".to_string(), "light".to_string(), false) }
};
let response = next.run(req).await; let response = next.run(req).await;
@@ -145,6 +158,20 @@ where
if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() { if let Some(template_response) = response.extensions().get::<TemplateResponse>().cloned() {
let template_engine = state.template_engine(); 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 // Helper to forward relevant headers
fn forward_headers(from: &axum::http::HeaderMap, to: &mut axum::http::HeaderMap) { fn forward_headers(from: &axum::http::HeaderMap, to: &mut axum::http::HeaderMap) {
for &header_name in HTMX_HEADERS_TO_FORWARD { 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 { let context = ContextWrapper {
user_theme: &user_theme, user_theme: &user_theme,
initial_theme: &initial_theme, initial_theme: &initial_theme,
is_authenticated, is_authenticated,
context: &template_response.context, user: current_user.as_ref(),
conversation_archive,
context: context_map,
}; };
match &template_response.template_kind { match &template_response.template_kind {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,16 +14,13 @@ use crate::middlewares::{
response_middleware::{HtmlError, TemplateResponse}, response_middleware::{HtmlError, TemplateResponse},
}; };
use common::storage::types::{ use common::storage::types::{
conversation::Conversation, ingestion_payload::IngestionPayload, ingestion_task::IngestionTask, ingestion_payload::IngestionPayload, ingestion_task::IngestionTask, scratchpad::Scratchpad,
scratchpad::Scratchpad, user::User,
}; };
#[derive(Serialize)] #[derive(Serialize)]
pub struct ScratchpadPageData { pub struct ScratchpadPageData {
user: User,
scratchpads: Vec<ScratchpadListItem>, scratchpads: Vec<ScratchpadListItem>,
archived_scratchpads: Vec<ScratchpadArchiveItem>, archived_scratchpads: Vec<ScratchpadArchiveItem>,
conversation_archive: Vec<Conversation>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
new_scratchpad: Option<ScratchpadDetail>, new_scratchpad: Option<ScratchpadDetail>,
} }
@@ -38,9 +35,7 @@ pub struct ScratchpadListItem {
#[derive(Serialize)] #[derive(Serialize)]
pub struct ScratchpadDetailData { pub struct ScratchpadDetailData {
user: User,
scratchpad: ScratchpadDetail, scratchpad: ScratchpadDetail,
conversation_archive: Vec<Conversation>,
is_editing_title: bool, is_editing_title: bool,
} }
@@ -135,7 +130,6 @@ pub async fn show_scratchpad_page(
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
let scratchpads = Scratchpad::get_by_user(&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 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> = let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect(); scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -149,10 +143,8 @@ pub async fn show_scratchpad_page(
"scratchpad/base.html", "scratchpad/base.html",
"main", "main",
ScratchpadPageData { ScratchpadPageData {
user,
scratchpads: scratchpad_list, scratchpads: scratchpad_list,
archived_scratchpads: archived_list, archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None, new_scratchpad: None,
}, },
)) ))
@@ -160,10 +152,8 @@ pub async fn show_scratchpad_page(
Ok(TemplateResponse::new_template( Ok(TemplateResponse::new_template(
"scratchpad/base.html", "scratchpad/base.html",
ScratchpadPageData { ScratchpadPageData {
user,
scratchpads: scratchpad_list, scratchpads: scratchpad_list,
archived_scratchpads: archived_list, archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None, new_scratchpad: None,
}, },
)) ))
@@ -177,7 +167,6 @@ pub async fn show_scratchpad_modal(
Query(query): Query<EditTitleQuery>, Query(query): Query<EditTitleQuery>,
) -> Result<impl IntoResponse, HtmlError> { ) -> Result<impl IntoResponse, HtmlError> {
let scratchpad = Scratchpad::get_by_id(&scratchpad_id, &user.id, &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?;
let scratchpad_detail = ScratchpadDetail::from(&scratchpad); let scratchpad_detail = ScratchpadDetail::from(&scratchpad);
@@ -187,9 +176,7 @@ pub async fn show_scratchpad_modal(
Ok(TemplateResponse::new_template( Ok(TemplateResponse::new_template(
"scratchpad/editor_modal.html", "scratchpad/editor_modal.html",
ScratchpadDetailData { ScratchpadDetailData {
user,
scratchpad: scratchpad_detail, scratchpad: scratchpad_detail,
conversation_archive,
is_editing_title, is_editing_title,
}, },
)) ))
@@ -206,7 +193,6 @@ pub async fn create_scratchpad(
let scratchpads = Scratchpad::get_by_user(&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 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> = let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect(); scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -219,10 +205,8 @@ pub async fn create_scratchpad(
"scratchpad/base.html", "scratchpad/base.html",
"main", "main",
ScratchpadPageData { ScratchpadPageData {
user,
scratchpads: scratchpad_list, scratchpads: scratchpad_list,
archived_scratchpads: archived_list, archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: Some(ScratchpadDetail::from(&scratchpad)), 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?; 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 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( Ok(TemplateResponse::new_template(
"scratchpad/editor_modal.html", "scratchpad/editor_modal.html",
ScratchpadDetailData { ScratchpadDetailData {
user,
scratchpad: ScratchpadDetail::from(&scratchpad), scratchpad: ScratchpadDetail::from(&scratchpad),
conversation_archive,
is_editing_title: false, is_editing_title: false,
}, },
)) ))
@@ -279,7 +260,6 @@ pub async fn delete_scratchpad(
// Return the updated main section content // Return the updated main section content
let scratchpads = Scratchpad::get_by_user(&user.id, &state.db).await?; 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 archived_scratchpads = Scratchpad::get_archived_by_user(&user.id, &state.db).await?;
let scratchpad_list: Vec<ScratchpadListItem> = let scratchpad_list: Vec<ScratchpadListItem> =
@@ -293,10 +273,8 @@ pub async fn delete_scratchpad(
"scratchpad/base.html", "scratchpad/base.html",
"main", "main",
ScratchpadPageData { ScratchpadPageData {
user,
scratchpads: scratchpad_list, scratchpads: scratchpad_list,
archived_scratchpads: archived_list, archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None, new_scratchpad: None,
}, },
)) ))
@@ -350,7 +328,6 @@ pub async fn ingest_scratchpad(
let scratchpads = Scratchpad::get_by_user(&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 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> = let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect(); scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -374,10 +351,8 @@ pub async fn ingest_scratchpad(
"scratchpad/base.html", "scratchpad/base.html",
"main", "main",
ScratchpadPageData { ScratchpadPageData {
user,
scratchpads: scratchpad_list, scratchpads: scratchpad_list,
archived_scratchpads: archived_list, archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None, new_scratchpad: None,
}, },
); );
@@ -399,7 +374,6 @@ pub async fn archive_scratchpad(
let scratchpads = Scratchpad::get_by_user(&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 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> = let scratchpad_list: Vec<ScratchpadListItem> =
scratchpads.iter().map(ScratchpadListItem::from).collect(); scratchpads.iter().map(ScratchpadListItem::from).collect();
@@ -411,15 +385,59 @@ pub async fn archive_scratchpad(
Ok(TemplateResponse::new_template( Ok(TemplateResponse::new_template(
"scratchpad/base.html", "scratchpad/base.html",
ScratchpadPageData { ScratchpadPageData {
user,
scratchpads: scratchpad_list, scratchpads: scratchpad_list,
archived_scratchpads: archived_list, archived_scratchpads: archived_list,
conversation_archive,
new_scratchpad: None, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -509,52 +527,3 @@ mod tests {
assert_eq!(archive_item.ingested_at, None); 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, response::IntoResponse,
}; };
use common::storage::types::{ use common::storage::types::{
conversation::Conversation,
text_content::{deserialize_flexible_id, TextContent}, text_content::{deserialize_flexible_id, TextContent},
user::User,
StoredObject, StoredObject,
}; };
use retrieval_pipeline::{RetrievalConfig, SearchResult, SearchTarget, StrategyOutput}; use retrieval_pipeline::{RetrievalConfig, SearchResult, SearchTarget, StrategyOutput};
@@ -194,10 +192,7 @@ pub async fn search_result_handler(
pub struct AnswerData { pub struct AnswerData {
search_result: Vec<SearchResultForTemplate>, search_result: Vec<SearchResultForTemplate>,
query_param: String, 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) = let (search_results_for_template, final_query_param_for_template) = if let Some(actual_query) =
params.query params.query
@@ -346,8 +341,6 @@ pub async fn search_result_handler(
AnswerData { AnswerData {
search_result: search_results_for_template, search_result: search_results_for_template,
query_param: final_query_param_for_template, query_param: final_query_param_for_template,
user,
conversation_archive,
}, },
)) ))
} }