From 9322434daa83a7a4316a450c36ddea3b1b6aa318 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 3 Jul 2026 20:14:59 +0700 Subject: [PATCH] feat(leptos): messages module skeleton + use_messages hook --- .../messages/components/image_grid.rs | 8 + .../messages/components/message_card.rs | 8 + .../messages/components/message_feed.rs | 8 + .../src/features/messages/components/mod.rs | 3 + .../src/features/messages/hooks/mod.rs | 1 + .../features/messages/hooks/use_messages.rs | 201 ++++++++++++++++++ .../frontend/src/features/messages/mod.rs | 11 + .../frontend/src/features/mod.rs | 1 + services/frontend-leptos/frontend/src/lib.rs | 1 + 9 files changed, 242 insertions(+) create mode 100644 services/frontend-leptos/frontend/src/features/messages/components/image_grid.rs create mode 100644 services/frontend-leptos/frontend/src/features/messages/components/message_card.rs create mode 100644 services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs create mode 100644 services/frontend-leptos/frontend/src/features/messages/components/mod.rs create mode 100644 services/frontend-leptos/frontend/src/features/messages/hooks/mod.rs create mode 100644 services/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs create mode 100644 services/frontend-leptos/frontend/src/features/messages/mod.rs create mode 100644 services/frontend-leptos/frontend/src/features/mod.rs diff --git a/services/frontend-leptos/frontend/src/features/messages/components/image_grid.rs b/services/frontend-leptos/frontend/src/features/messages/components/image_grid.rs new file mode 100644 index 0000000..55cca3f --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/components/image_grid.rs @@ -0,0 +1,8 @@ +use leptos::prelude::*; + +#[component] +pub fn ImageGrid() -> impl IntoView { + view! { +
"Image Grid (placeholder)"
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/messages/components/message_card.rs b/services/frontend-leptos/frontend/src/features/messages/components/message_card.rs new file mode 100644 index 0000000..a38b482 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/components/message_card.rs @@ -0,0 +1,8 @@ +use leptos::prelude::*; + +#[component] +pub fn MessageCard() -> impl IntoView { + view! { +
"Message Card (placeholder)"
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs b/services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs new file mode 100644 index 0000000..057e1d4 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs @@ -0,0 +1,8 @@ +use leptos::prelude::*; + +#[component] +pub fn MessageFeed() -> impl IntoView { + view! { +
"Message Feed (placeholder)"
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/messages/components/mod.rs b/services/frontend-leptos/frontend/src/features/messages/components/mod.rs new file mode 100644 index 0000000..644a7cd --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/components/mod.rs @@ -0,0 +1,3 @@ +pub mod message_feed; +pub mod message_card; +pub mod image_grid; diff --git a/services/frontend-leptos/frontend/src/features/messages/hooks/mod.rs b/services/frontend-leptos/frontend/src/features/messages/hooks/mod.rs new file mode 100644 index 0000000..4533575 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/hooks/mod.rs @@ -0,0 +1 @@ +pub mod use_messages; diff --git a/services/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs b/services/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs new file mode 100644 index 0000000..4bbac14 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs @@ -0,0 +1,201 @@ +use leptos::prelude::*; +use shared_types::message::{MessageRecord, PageResult}; +use crate::api::messages::{get_messages, reanalyze_message, reanalyze_batch}; +use std::collections::HashMap; +use std::sync::Arc; +use wasm_bindgen_futures::spawn_local; + +/// Merges current messages with incoming messages, deduplicating by ID and sorting +pub fn merge_messages(current: &[MessageRecord], incoming: &[MessageRecord]) -> Vec { + let mut by_id: HashMap = current.iter().map(|m| (m.id.clone(), m.clone())).collect(); + for msg in incoming { + by_id.insert(msg.id.clone(), msg.clone()); + } + let mut merged: Vec = by_id.into_values().collect(); + merged.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id))); + merged +} + +/// Callback type for fetch_messages +pub type FetchMessagesCallback = Arc; +/// Callback type for load_more +pub type LoadMoreCallback = Arc; +/// Callback type for reanalyze +pub type ReanalyzeCallback = Arc; +/// Callback type for reanalyze_all_errors +pub type ReanalyzeAllErrorsCallback = Arc; + +/// State returned by use_messages hook +#[derive(Clone)] +pub struct MessagesState { + /// Current list of messages + pub messages: RwSignal>, + /// Whether the initial fetch is in progress + pub loading: ReadSignal, + /// Whether we're loading more messages + pub loading_more: RwSignal, + /// Pagination cursor for next page + pub cursor: RwSignal>, + /// Derived: whether there are more messages to load + pub has_more: Memo, + /// Last error message if any + pub error: RwSignal>, + /// Current guild ID + pub current_guild: RwSignal>, + /// Fetch initial messages for a guild + pub fetch_messages: FetchMessagesCallback, + /// Load next page of messages + pub load_more: LoadMoreCallback, + /// Reanalyze a single message + pub reanalyze: ReanalyzeCallback, + /// Reanalyze all error messages in current batch + pub reanalyze_all_errors: ReanalyzeAllErrorsCallback, +} + +/// Hook to manage message data fetching and state +#[component] +pub fn use_messages() -> MessagesState { + // Core signals + let messages_signal = RwSignal::new(Vec::::new()); + let (loading, set_loading) = create_signal(false); + let loading_more_signal = RwSignal::new(false); + let cursor_signal = RwSignal::new(None::); + let error_signal = RwSignal::new(None::); + let current_guild_signal = RwSignal::new(None::); + + // Derived signal: has_more is true if cursor is Some + let has_more_signal = create_memo(move |_| cursor_signal.get().is_some()); + + // Fetch initial messages for a guild + let fetch_messages_impl = Arc::new(move |guild_id: String| { + spawn_local({ + let guild_id = guild_id.clone(); + async move { + error_signal.set(None); + set_loading.set(true); + + match get_messages(&guild_id, Some(30), None, None).await { + Ok(PageResult { data, next_cursor }) => { + messages_signal.set(data); + cursor_signal.set(next_cursor); + current_guild_signal.set(Some(guild_id)); + set_loading.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to fetch messages: {}", e))); + set_loading.set(false); + } + } + } + }); + }); + + // Load more messages (append next page) + let load_more_impl = Arc::new(move || { + spawn_local(async move { + let guild_id = match current_guild_signal.get() { + Some(id) => id, + None => { + error_signal.set(Some("No guild selected".to_string())); + return; + } + }; + + let cursor = match cursor_signal.get() { + Some(c) => c, + None => { + error_signal.set(Some("No more messages to load".to_string())); + return; + } + }; + + loading_more_signal.set(true); + error_signal.set(None); + + match get_messages(&guild_id, Some(30), None, Some(&cursor)).await { + Ok(PageResult { data, next_cursor }) => { + let current = messages_signal.get(); + messages_signal.set(merge_messages(¤t, &data)); + cursor_signal.set(next_cursor); + loading_more_signal.set(false); + } + Err(e) => { + error_signal.set(Some(format!("Failed to load more: {}", e))); + loading_more_signal.set(false); + } + } + }); + }); + + // Reanalyze single message with optimistic update + let reanalyze_impl = Arc::new(move |message_id: String| { + spawn_local({ + let message_id = message_id.clone(); + async move { + // Optimistic: flip status to Processing + let mut msgs = messages_signal.get(); + if let Some(pos) = msgs.iter().position(|m| m.id == message_id) { + if let Some(ref mut msg) = msgs.get_mut(pos) { + msg.ai_status = Some(shared_types::message::AiStatus::Processing); + } + } + messages_signal.set(msgs); + + // Call API + match reanalyze_message(&message_id).await { + Ok(_) => { + // Success: keep the Processing status (will be updated via WS) + } + Err(e) => { + // Revert to Error status on failure + let mut msgs = messages_signal.get(); + if let Some(pos) = msgs.iter().position(|m| m.id == message_id) { + if let Some(ref mut msg) = msgs.get_mut(pos) { + msg.ai_status = Some(shared_types::message::AiStatus::Error); + msg.ai_error = Some(e.to_string()); + } + } + messages_signal.set(msgs); + error_signal.set(Some(format!("Reanalyze failed: {}", e))); + } + } + } + }); + }); + + // Reanalyze all error messages + let reanalyze_all_errors_impl = Arc::new(move || { + spawn_local(async move { + match reanalyze_batch().await { + Ok(_count) => { + error_signal.set(None); + // Optimistically mark all error messages as Processing + let mut msgs = messages_signal.get(); + for msg in msgs.iter_mut() { + if msg.ai_status == Some(shared_types::message::AiStatus::Error) { + msg.ai_status = Some(shared_types::message::AiStatus::Processing); + } + } + messages_signal.set(msgs); + } + Err(e) => { + error_signal.set(Some(format!("Batch reanalyze failed: {}", e))); + } + } + }); + }); + + MessagesState { + messages: messages_signal, + loading, + loading_more: loading_more_signal, + cursor: cursor_signal, + has_more: has_more_signal, + error: error_signal, + current_guild: current_guild_signal, + fetch_messages: fetch_messages_impl, + load_more: load_more_impl, + reanalyze: reanalyze_impl, + reanalyze_all_errors: reanalyze_all_errors_impl, + } +} diff --git a/services/frontend-leptos/frontend/src/features/messages/mod.rs b/services/frontend-leptos/frontend/src/features/messages/mod.rs new file mode 100644 index 0000000..5bb1d17 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/messages/mod.rs @@ -0,0 +1,11 @@ +use leptos::prelude::*; + +pub mod components; +pub mod hooks; + +#[component] +pub fn MessagesPanel() -> impl IntoView { + view! { +
"Messages Panel (loading...)"
+ } +} diff --git a/services/frontend-leptos/frontend/src/features/mod.rs b/services/frontend-leptos/frontend/src/features/mod.rs new file mode 100644 index 0000000..ba63992 --- /dev/null +++ b/services/frontend-leptos/frontend/src/features/mod.rs @@ -0,0 +1 @@ +pub mod messages; diff --git a/services/frontend-leptos/frontend/src/lib.rs b/services/frontend-leptos/frontend/src/lib.rs index 74a9c4d..f00e055 100644 --- a/services/frontend-leptos/frontend/src/lib.rs +++ b/services/frontend-leptos/frontend/src/lib.rs @@ -1,6 +1,7 @@ pub mod api; pub mod app; pub mod auth; +pub mod features; pub mod layout; pub mod ui; pub mod ws;