diff --git a/services/frontend-leptos/frontend/src/app.rs b/services/frontend-leptos/frontend/src/app.rs index 7aaf4f7..c5e9bec 100644 --- a/services/frontend-leptos/frontend/src/app.rs +++ b/services/frontend-leptos/frontend/src/app.rs @@ -92,9 +92,9 @@ pub fn App() -> impl IntoView { // Content area
{move || match ui.active_tab.get() { - Tab::Messages => view! { }.into_view(), - Tab::Live => view! {
"Live Panel"
}.into_view(), - Tab::Dashboard => view! {
"Dashboard Panel"
}.into_view(), + Tab::Messages => view! { }.into_any(), + Tab::Live => view! {
"Live Panel"
}.into_any(), + Tab::Dashboard => view! {
"Dashboard Panel"
}.into_any(), }}
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 index 4c24941..af88917 100644 --- a/services/frontend-leptos/frontend/src/features/messages/components/message_card.rs +++ b/services/frontend-leptos/frontend/src/features/messages/components/message_card.rs @@ -1,6 +1,6 @@ use leptos::prelude::*; use regex::Regex; -use shared_types::message::{AiSeverity, AiStatus, MessageRecord}; +use shared_types::message::{AiSeverity, AiStatus, AttachmentRef, MessageRecord}; use std::sync::{Arc, OnceLock}; use wasm_bindgen::prelude::*; @@ -84,13 +84,13 @@ fn get_cats(raw: &Option>) -> Vec { // ─── StatusBadgeInline ──────────────────────────────────── #[component] fn StatusBadgeInline(status: AiStatus) -> impl IntoView { - let (cl, icon_svg) = match &status { - AiStatus::Clean => ("status-badge-clean", Some(view! { }).into_any()), - AiStatus::Flagged => ("status-badge-flagged", Some(view! { }).into_any()), - AiStatus::Error => ("status-badge-error", Some(view! { }).into_any()), - AiStatus::Pending => ("status-badge-pending", None.into_any()), - AiStatus::Processing => ("status-badge-processing", None.into_any()), - AiStatus::Warn => ("status-badge-warn", None.into_any()), + let (cl, icon_svg): (&'static str, AnyView) = match &status { + AiStatus::Clean => ("status-badge-clean", view! { }.into_any()), + AiStatus::Flagged => ("status-badge-flagged", view! { }.into_any()), + AiStatus::Error => ("status-badge-error", view! { }.into_any()), + AiStatus::Pending => ("status-badge-pending", view! { }.into_any()), + AiStatus::Processing => ("status-badge-processing", view! { }.into_any()), + AiStatus::Warn => ("status-badge-warn", view! { }.into_any()), }; view! { @@ -125,20 +125,20 @@ pub fn MessageRow( // Attachments let all_atts = message.metadata.as_ref() .and_then(|m| m.attachments.as_ref()).cloned().unwrap_or_default(); - let imgs: Vec<_> = all_atts.iter().filter(|a| { + let imgs: Vec = all_atts.iter().filter(|a| { a.content_type.as_deref().map(|ct| ct.starts_with("image/")).unwrap_or(false) || a.name.to_lowercase().ends_with(".png") || a.name.to_lowercase().ends_with(".jpg") || a.name.to_lowercase().ends_with(".jpeg") || a.name.to_lowercase().ends_with(".gif") || a.name.to_lowercase().ends_with(".webp") - }).collect(); - let vids: Vec<_> = all_atts.iter().filter(|a| { + }).cloned().collect(); + let vids: Vec = all_atts.iter().filter(|a| { a.content_type.as_deref().map(|ct| ct.starts_with("video/")).unwrap_or(false) || a.name.to_lowercase().ends_with(".mp4") || a.name.to_lowercase().ends_with(".webm") || a.name.to_lowercase().ends_with(".mov") - }).collect(); + }).cloned().collect(); let stickers = message.metadata.as_ref() .and_then(|m| m.stickers.as_ref()).cloned().unwrap_or_default(); @@ -180,9 +180,10 @@ pub fn MessageRow( {/* Content */} {show.then(|| { let rendered = render_emojis(display); - let cls = if message.deleted_at.is_some() { "text-secondary/60" } else { "" }; + let cls_str = if message.deleted_at.is_some() { "text-secondary/60" } else { "" }; + let class_str = format!("whitespace-pre-wrap break-words text-sm leading-6 {}", cls_str); view! { -

+

{rendered.into_iter().collect::>()}

} @@ -192,17 +193,22 @@ pub fn MessageRow( {(!stickers.is_empty()).then(|| view! {
{stickers.iter().map(|s| { - let url = s.url.clone(); - let name = s.name.clone().unwrap_or_default(); + let url_owned = s.url.clone().unwrap_or_default(); + let name_owned = s.name.clone().unwrap_or_default(); + let has_url = !url_owned.is_empty(); view! {
- {url.as_ref().map(|u| view! { - &name - }).unwrap_or_else(|| view! { -
- "😊" -
- })} + {if has_url { + view! { + name_owned + }.into_any() + } else { + view! { +
+ "😊" +
+ }.into_any() + }}
} }).collect::>()} @@ -210,55 +216,94 @@ pub fn MessageRow( })} {/* Images */} - {(!imgs.is_empty()).then(|| view! { -
- {imgs.iter().take(4).map(|a| view! { - - &a.name + {if !imgs.is_empty() { + let imgs_local = imgs.clone(); + let images_view = imgs_local.iter().take(4).map(|a| { + let url1 = a.url.clone(); + let url2 = a.url.clone(); + let name1 = a.name.clone(); + view! { + + name1 - }).collect::>()} - {(imgs.len() > 4).then(|| view! { + } + }).collect::>(); + let overflow = if imgs.len() > 4 { + let extra = imgs.len() - 4; + view! {
- {"+"} {imgs.len() - 4} "🖼" + {"+"} {extra} "🖼"
- })} -
- })} + }.into_any() + } else { + view! {}.into_any() + }; + view! { +
+ {images_view} + {overflow} +
+ }.into_any() + } else { + view! {}.into_any() + }} {/* Videos */} - {(!vids.is_empty()).then(|| view! { -
- {vids.iter().take(4).map(|a| view! { - - }).collect::>()} - {(vids.len() > 4).then(|| view! { + {if !vids.is_empty() { + let vids_local = vids.clone(); + let videos_view = vids_local.iter().take(4).map(|a| { + let url = a.url.clone(); + view! { + + } + }).collect::>(); + let overflow = if vids.len() > 4 { + let extra = vids.len() - 4; + view! {
- {"+"} {vids.len() - 4} "▶" + {"+"} {extra} "▶"
- })} -
- })} + }.into_any() + } else { + view! {}.into_any() + }; + view! { +
+ {videos_view} + {overflow} +
+ }.into_any() + } else { + view! {}.into_any() + }} {/* Categories */} - {(!cats.is_empty()).then(|| view! { -
- {cats.iter().map(|c| view! { - {c.clone()} - }).collect::>()} -
- })} + {if !cats.is_empty() { + let cats_local = cats.clone(); + view! { +
+ {cats_local.iter().map(|c| view! { + {c.clone()} + }).collect::>()} +
+ }.into_any() + } else { + view! {}.into_any() + }} {/* AI Analysis */} {message.ai_analysis.as_ref().map(|analysis| { - let border = if ai_st == AiStatus::Flagged { "border-l-3 bg-warning/5" } else { "border-l-3 bg-success/5" }; + let border_str = if ai_st == AiStatus::Flagged { "border-l-3 bg-warning/5" } else { "border-l-3 bg-success/5" }; let icon = if ai_st == AiStatus::Flagged { "🚨" } else { "ℹ️" }; + let analysis_summary_str = analysis_summary.clone(); + let analysis_str = analysis.clone(); view! { -
+
{icon}
- {&analysis_summary} -
{analysis}
+ {analysis_summary_str} +
{analysis_str}
@@ -266,10 +311,13 @@ pub fn MessageRow( })} {/* Error */} - {message.ai_error.as_ref().map(|e| view! { -
- "AI error: "{e} -
+ {message.ai_error.as_ref().map(|e| { + let error_str = e.clone(); + view! { +
+ "AI error: "{error_str} +
+ } })} {/* Re-analyze */} @@ -316,11 +364,14 @@ pub fn MessageCard(
- {&first.username} - {loc_label.as_ref().map(|l| view! { - - "#" " " {l} - + {first.username.clone()} + {loc_label.as_ref().map(|l| { + let label_str = l.clone(); + view! { + + "#" " " {label_str} + + } })} {time_ago(first.created_at)} 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 index cced974..6f1758b 100644 --- a/services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs +++ b/services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs @@ -43,6 +43,7 @@ pub fn MessageFeed( create_effect(move |_| { let _ = intersecting.get(); // track signal if let Some(node) = sentinel_ref.get() { + let on_load_more = on_load_more.clone(); let cb = Closure::)>::new(move |entries: Vec| { for entry in entries { if let Some(entry) = entry.dyn_ref::() { diff --git a/services/frontend-leptos/frontend/src/features/messages/mod.rs b/services/frontend-leptos/frontend/src/features/messages/mod.rs index 07a724a..5af8065 100644 --- a/services/frontend-leptos/frontend/src/features/messages/mod.rs +++ b/services/frontend-leptos/frontend/src/features/messages/mod.rs @@ -53,10 +53,10 @@ pub fn MessagesPanel() -> impl IntoView { }).collect() }); - // Search handler - let handle_search = { + // Search handler - takes any event type and triggers the search + let do_search = { let q = search_query; - move |_| { + move || { let query = q.get(); if query.trim().is_empty() { set_show_search.set(false); @@ -79,6 +79,9 @@ pub fn MessagesPanel() -> impl IntoView { }); } }; + // Separate closures for different event types so on:click/on:keydown type-check + let handle_search_click = move |_: web_sys::MouseEvent| do_search(); + let handle_search_keydown = move |_: web_sys::KeyboardEvent| do_search(); // Clear search let clear_search = move |_| { @@ -141,13 +144,22 @@ pub fn MessagesPanel() -> impl IntoView { create_effect(move |_| { if let Some(config) = use_context::() { if let Some(ref guild_id) = config.monitor_guild_id { - state.fetch_messages(guild_id.clone()); + (state.fetch_messages)(guild_id.clone()); } } }); // ─── View ──────────────────────────────────────────────── - let (total, clean, flagged, error, pending, deleted, edited) = move || stats.get(); + let get_stats = move || stats.get(); + let (total, clean, flagged, error, pending, deleted, edited) = ( + move || get_stats().0, + move || get_stats().1, + move || get_stats().2, + move || get_stats().3, + move || get_stats().4, + move || get_stats().5, + move || get_stats().6, + ); view! {
@@ -190,15 +202,15 @@ pub fn MessagesPanel() -> impl IntoView { prop:value=search_query on:input=move |ev| set_search_query.set(event_target_value(&ev)) on:keydown=move |ev| { - if ev.key() == "Enter" { handle_search(()); } + if ev.key() == "Enter" { handle_search_keydown(ev); } } - disabled=is_searching.get() + disabled=move || is_searching.get() />
@@ -211,7 +223,7 @@ pub fn MessagesPanel() -> impl IntoView {
- ) + { + let load_more_cb = state.load_more.clone(); + let empty_text: &'static str = if show_search.get() { "No messages found matching your search." } else { "No captures yet." }; + let has_more = if show_search.get() { false } else { state.has_more.get() }; + view! { + + on_reanalyze=state.reanalyze.clone() + /> } - on_reanalyze=state.reanalyze.clone() - /> + }
diff --git a/services/frontend-leptos/shared-types/src/message.rs b/services/frontend-leptos/shared-types/src/message.rs index 5f156d4..1d9e696 100644 --- a/services/frontend-leptos/shared-types/src/message.rs +++ b/services/frontend-leptos/shared-types/src/message.rs @@ -34,7 +34,7 @@ pub enum AiRecommendedAction { } // ── Message Metadata ────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct MessageMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub stickers: Option>, @@ -46,7 +46,7 @@ pub struct MessageMetadata { pub channel: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct StickerInfo { #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, @@ -54,7 +54,7 @@ pub struct StickerInfo { pub url: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AttachmentRef { pub name: String, pub url: String, @@ -63,7 +63,7 @@ pub struct AttachmentRef { pub content_type: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct EmbedInfo { #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, @@ -73,7 +73,7 @@ pub struct EmbedInfo { pub thumbnail: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct EmbedMedia { pub url: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -82,7 +82,7 @@ pub struct EmbedMedia { pub height: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ChannelRef { pub channel_id: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -94,7 +94,7 @@ pub struct ChannelRef { } // ── Message Record ──────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct MessageRecord { pub id: String, pub guild_id: String, @@ -140,7 +140,7 @@ pub struct MessageRecord { } // ── Pagination ──────────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PageResult { pub data: Vec, #[serde(rename = "nextCursor")] @@ -148,7 +148,7 @@ pub struct PageResult { } // ── Attachment ──────────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AttachmentRecord { pub id: String, pub message_id: String,