diff --git a/services/frontend-leptos/.env.example b/services/frontend-leptos/.env.example new file mode 100644 index 0000000..3634e80 --- /dev/null +++ b/services/frontend-leptos/.env.example @@ -0,0 +1,3 @@ +# API/WS endpoints — set these before building +VITE_BE_API_URL=http://localhost:3001 +VITE_BE_WS_URL=ws://localhost:3001/ws diff --git a/services/frontend-leptos/Cargo.toml b/services/frontend-leptos/Cargo.toml new file mode 100644 index 0000000..3ff0eb7 --- /dev/null +++ b/services/frontend-leptos/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["shared-types", "frontend"] diff --git a/services/frontend-leptos/rust-toolchain.toml b/services/frontend-leptos/rust-toolchain.toml new file mode 100644 index 0000000..3aa3203 --- /dev/null +++ b/services/frontend-leptos/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2026-06-01" +components = ["rust-src", "rustc-dev"] +targets = ["wasm32-unknown-unknown"] diff --git a/services/frontend-leptos/shared-types/Cargo.toml b/services/frontend-leptos/shared-types/Cargo.toml new file mode 100644 index 0000000..3f27442 --- /dev/null +++ b/services/frontend-leptos/shared-types/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "shared-types" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] } diff --git a/services/frontend-leptos/shared-types/src/dashboard.rs b/services/frontend-leptos/shared-types/src/dashboard.rs new file mode 100644 index 0000000..33eafbb --- /dev/null +++ b/services/frontend-leptos/shared-types/src/dashboard.rs @@ -0,0 +1,83 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DashboardStats { + pub total_messages: u64, + pub total_users: u64, + pub total_flagged: u64, + pub total_clean: u64, + pub total_warned: u64, + pub total_error: u64, + pub total_voice_recordings: u64, + pub total_profiles: u64, + pub today_messages: u64, + pub today_flagged: u64, + pub active_users_24h: u64, + pub top_channels: Vec, + pub moderation_overview: ModerationOverview, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopChannel { + pub channel_id: String, + pub channel_name: String, + pub message_count: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModerationOverview { + pub pending: u64, + pub processing: u64, + pub error: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DashboardUser { + pub user_id: String, + pub username: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile_summary: Option, + pub total_messages: u64, + pub flagged_count: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_message_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub trust_score: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DashboardUserDetail { + #[serde(flatten)] + pub user: DashboardUser, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_analyzed_at: Option, + pub clean_message_streak: u64, + pub total_infractions: u64, + pub clean_count: u64, + pub recent_messages: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DashboardChannel { + pub channel_id: String, + pub channel_name: String, + pub guild_id: String, + pub total_messages: u64, + pub flagged_count: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_message_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub culture_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_analyzed_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DashboardChannelDetail { + #[serde(flatten)] + pub channel: DashboardChannel, + pub clean_count: u64, + pub recent_messages: Vec, +} diff --git a/services/frontend-leptos/shared-types/src/guild.rs b/services/frontend-leptos/shared-types/src/guild.rs new file mode 100644 index 0000000..85ecc4c --- /dev/null +++ b/services/frontend-leptos/shared-types/src/guild.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Guild { + pub id: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Channel { + pub id: String, + pub name: String, + #[serde(rename = "type")] + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GuildVoiceEntry { + pub guild_id: String, + pub channel_id: String, + pub channel_name: String, + pub connected_at: i64, +} + +// ── Config ──────────────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub monitor_guild_id: Option, +} diff --git a/services/frontend-leptos/shared-types/src/lib.rs b/services/frontend-leptos/shared-types/src/lib.rs new file mode 100644 index 0000000..1ca4889 --- /dev/null +++ b/services/frontend-leptos/shared-types/src/lib.rs @@ -0,0 +1,7 @@ +pub mod message; +pub mod guild; +pub mod voice; +pub mod media; +pub mod dashboard; +pub mod recording; +pub mod ui_state; diff --git a/services/frontend-leptos/shared-types/src/media.rs b/services/frontend-leptos/shared-types/src/media.rs new file mode 100644 index 0000000..c175dd1 --- /dev/null +++ b/services/frontend-leptos/shared-types/src/media.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +pub type MediaMode = String; // "music" | "screen" + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MediaItem { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(rename = "durationMs")] + #[serde(skip_serializing_if = "Option::is_none")] + pub duration_ms: Option, + #[serde(rename = "thumbnailUrl")] + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MediaState { + pub playing: bool, + #[serde(rename = "musicVolume")] + pub music_volume: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub current: Option, + pub queue: Vec, +} diff --git a/services/frontend-leptos/shared-types/src/message.rs b/services/frontend-leptos/shared-types/src/message.rs new file mode 100644 index 0000000..5f156d4 --- /dev/null +++ b/services/frontend-leptos/shared-types/src/message.rs @@ -0,0 +1,170 @@ +use serde::{Deserialize, Serialize}; + +// ── AI Status ───────────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum AiStatus { + Pending, + Processing, + Clean, + Warn, + Flagged, + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum AiSeverity { + None, + Low, + Medium, + High, + Critical, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum AiRecommendedAction { + None, + Monitor, + Warn, + Review, + Delete, + Escalate, +} + +// ── Message Metadata ────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MessageMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub stickers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub embeds: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StickerInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AttachmentRef { + pub name: String, + pub url: String, + #[serde(rename = "contentType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub content_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbedInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbedMedia { + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChannelRef { + pub channel_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_name: Option, +} + +// ── Message Record ──────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessageRecord { + pub id: String, + pub guild_id: String, + pub channel_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + pub user_id: String, + pub username: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + pub content: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub edited_content: Option, + #[serde(rename = "type")] + pub msg_type: String, // "text" | "edited" | "deleted" + pub created_at: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub edited_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_severity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_confidence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_moderation_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_moderation_score: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_analysis: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_categories: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_recommended_action: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ai_analyzed_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +// ── Pagination ──────────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PageResult { + pub data: Vec, + #[serde(rename = "nextCursor")] + pub next_cursor: Option, +} + +// ── Attachment ──────────────────────────────────────────── +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AttachmentRecord { + pub id: String, + pub message_id: String, + pub guild_id: String, + pub channel_id: String, + pub filename: String, + pub size: u64, + #[serde(rename = "type")] + pub mime_type: String, + pub discord_url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub uploaded_url: Option, + pub upload_status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub upload_error: Option, + pub created_at: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub uploaded_at: Option, +} diff --git a/services/frontend-leptos/shared-types/src/recording.rs b/services/frontend-leptos/shared-types/src/recording.rs new file mode 100644 index 0000000..5cb8fba --- /dev/null +++ b/services/frontend-leptos/shared-types/src/recording.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VoiceRecording { + pub id: String, + pub user_id: String, + pub username: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub guild_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_name: Option, + pub filename: String, + pub size_bytes: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub download_url: Option, + pub upload_status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub upload_error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub transcription: Option, + pub created_at: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub uploaded_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VoiceRecordingListResponse { + pub items: Vec, + #[serde(rename = "nextCursor")] + pub next_cursor: Option, + pub has_more: bool, +} diff --git a/services/frontend-leptos/shared-types/src/ui_state.rs b/services/frontend-leptos/shared-types/src/ui_state.rs new file mode 100644 index 0000000..f0eb826 --- /dev/null +++ b/services/frontend-leptos/shared-types/src/ui_state.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Tab { + Messages, + Live, + Dashboard, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UiState { + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_guild: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_voice_guild: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_voice_channel: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_text_guild: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub selected_text_channel: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub active_tab: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_listening: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_streaming: Option, +} diff --git a/services/frontend-leptos/shared-types/src/voice.rs b/services/frontend-leptos/shared-types/src/voice.rs new file mode 100644 index 0000000..888277c --- /dev/null +++ b/services/frontend-leptos/shared-types/src/voice.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; +use crate::guild::GuildVoiceEntry; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VoiceStatus { + pub connected: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub active_guild_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub active_channel_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub active_channel_name: Option, + pub connections: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActiveSpeaker { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + pub user_id: String, + pub username: String, + pub avatar: String, + pub speaking: bool, +}