mirror of
https://github.com/perstarkse/minne.git
synced 2026-02-22 08:07:40 +01:00
refactor: additional responsibilities to middleware, simplified handlers
fix
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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
@@ -65,3 +65,9 @@ impl ProvidesTemplateEngine for HtmlState {
|
||||
&self.templates
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::middlewares::response_middleware::ProvidesHtmlState for HtmlState {
|
||||
fn html_state(&self) -> &HtmlState {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
))
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
))
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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 },
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user