19 KiB
Phase 3: Messages Feature — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Implement the complete Messages panel — message list with infinite scroll, user-grouped cards, Discord emoji rendering, AI status filtering, full-text search, image grid, reanalyze actions, and WS-driven real-time updates.
Architecture: Leptos signals + Resources for state and data fetching. web-sys IntersectionObserver for infinite scroll sentinel. ws::context::WsContext drives real-time message_created/updated/deleted/analyzed events. Components follow the existing ui/* + layout/* patterns. CSS classes defined in Phase 2's app.css.
Tech Stack: Leptos 0.7 CSR, gloo-net fetch, web-sys (IntersectionObserver, console), serde/serde_json, lucide-leptos icons, plain CSS.
Global Constraints
- Branch:
leptos-rewriteat/mnt/code/bete/.worktrees/leptos-rewrite/ - Use
use leptos::prelude::*;(NOTuse leptos::*;) - Combine duplicate
class=attributes into singleformat!()strings #[serde(rename_all = "camelCase")]on structs where backend sends camelCase- All timestamps are
i64(millis since epoch), not strings - Rust types must match the JSON wire format from backend exactly
File Structure
services/frontend-leptos/frontend/src/
├── features/
│ ├── mod.rs # mod messages;
│ └── messages/
│ ├── mod.rs # MessagesPanel component
│ ├── components/
│ │ ├── mod.rs # mod message_feed message_card image_grid;
│ │ ├── message_feed.rs # Infinite scroll, user grouping
│ │ ├── message_card.rs # User-grouped message card
│ │ └── image_grid.rs # Masonry-like image grid
│ └── hooks/
│ ├── mod.rs # mod use_messages;
│ └── use_messages.rs # Data fetching, pagination, reanalyze
Files to modify:
services/frontend-leptos/frontend/src/lib.rs— addpub mod features;services/frontend-leptos/frontend/src/app.rs— wireMessagesPanelas default tab contentservices/frontend-leptos/frontend/src/app.css— add message-specific CSS classes
Files to create (8 new):
services/frontend-leptos/frontend/src/features/mod.rsservices/frontend-leptos/frontend/src/features/messages/mod.rsservices/frontend-leptos/frontend/src/features/messages/components/mod.rsservices/frontend-leptos/frontend/src/features/messages/components/message_feed.rsservices/frontend-leptos/frontend/src/features/messages/components/message_card.rsservices/frontend-leptos/frontend/src/features/messages/components/image_grid.rsservices/frontend-leptos/frontend/src/features/messages/hooks/mod.rsservices/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs
Task 1: Module skeleton + use_messages hook
Files:
- Create:
services/frontend-leptos/frontend/src/features/mod.rs - Create:
services/frontend-leptos/frontend/src/features/messages/mod.rs - Create:
services/frontend-leptos/frontend/src/features/messages/components/mod.rs - Create:
services/frontend-leptos/frontend/src/features/messages/hooks/mod.rs - Create:
services/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs - Modify:
services/frontend-leptos/frontend/src/lib.rs— addpub mod features; - Modify:
services/frontend-leptos/frontend/src/features/messages/mod.rs— add placeholderMessagesPanelcomponent
Interfaces:
- Consumes:
shared_types::message::{MessageRecord, PageResult}from shared-types crate,api::messages::{get_messages, search_messages, reanalyze_message, reanalyze_batch}from API client - Produces:
MessagesStatestruct returned byuse_messages()that later tasks wire intoMessagesPanel
Step 1: Create module files
services/frontend-leptos/frontend/src/features/mod.rs:
pub mod messages;
services/frontend-leptos/frontend/src/features/messages/hooks/mod.rs:
pub mod use_messages;
services/frontend-leptos/frontend/src/features/messages/components/mod.rs:
pub mod message_feed;
pub mod message_card;
pub mod image_grid;
services/frontend-leptos/frontend/src/features/messages/mod.rs (placeholder for now):
use leptos::prelude::*;
pub mod components;
pub mod hooks;
#[component]
pub fn MessagesPanel() -> impl IntoView {
view! {
<div>"Messages Panel (loading...)"</div>
}
}
Step 2: Create use_messages hook
services/frontend-leptos/frontend/src/features/messages/hooks/use_messages.rs:
This hook ports the React useMessages logic. It manages:
messages: RwSignal<Vec<MessageRecord>>— the message list, sorted by created_at descloading: Signal<bool>— loading stateloading_more: RwSignal<bool>— infinite scroll loadingcursor: RwSignal<Option<String>>— pagination cursorhas_more: Signal<bool>— derived from cursorerror: RwSignal<Option<String>>— last error messagecurrent_guild: RwSignal<Option<String>>— current guild IDfetch_messages(guild_id)— initial fetch (replaces messages)load_more()— append next pagereanalyze(id)— optimistic status flip + API call + revert on failurereanalyze_all_errors()— batch reanalyze error messages
The mergeMessages function ported from React:
pub fn merge_messages(current: &[MessageRecord], incoming: &[MessageRecord]) -> Vec<MessageRecord> {
let mut by_id: std::collections::HashMap<&str, &MessageRecord> = current.iter().map(|m| (m.id.as_str(), m)).collect();
for msg in incoming {
by_id.insert(msg.id.as_str(), msg);
}
let mut merged: Vec<MessageRecord> = by_id.into_values().cloned().collect();
merged.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
merged
}
Step 3: Verify compilation
Run: cargo check --manifest-path services/frontend-leptos/Cargo.toml from worktree root.
Step 4: Commit
git add services/frontend-leptos/frontend/src/features/ services/frontend-leptos/frontend/src/lib.rs
git commit -m "feat(leptos): messages module skeleton + use_messages hook"
Task 2: MessageFeed + message grouping
Files:
- Create:
services/frontend-leptos/frontend/src/features/messages/components/message_feed.rs - Modify:
services/frontend-leptos/frontend/src/features/messages/components/mod.rs— already done in Task 1
Interfaces:
- Consumes:
MessageRecordfrom shared-types,message_card::MessageCardandmessage_card::MessageCardSkeletonfrom next task - Produces:
MessageFeedcomponent with props:messages: Vec<MessageRecord>,on_reanalyze: Callback<String>,empty_text: String,has_more: bool,loading_more: bool,on_load_more: Callback<()>,loading: bool
Key logic (ported from React MessageFeed):
Group messages by same user within 5-minute window:
const GROUP_WINDOW_MS: i64 = 5 * 60 * 1000;
fn group_messages(messages: &[MessageRecord]) -> Vec<Vec<MessageRecord>> {
let mut groups: Vec<Vec<MessageRecord>> = Vec::new();
for msg in messages {
if let Some(last_group) = groups.last_mut() {
let same_user = last_group.first().map(|m| m.user_id == msg.user_id).unwrap_or(false);
let same_window = last_group.last().map(|m| (m.created_at - msg.created_at).abs() < GROUP_WINDOW_MS).unwrap_or(false);
if same_user && same_window {
last_group.push(msg.clone());
continue;
}
}
groups.push(vec![msg.clone()]);
}
groups
}
Infinite scroll sentinel (IntersectionObserver):
In Leptos, use web_sys::IntersectionObserver via create_effect + on_cleanup. Create a sentinel <div> at the bottom of the list. When it intersects, call on_load_more.
let sentinel_ref = create_node_ref::<html::Div>();
create_effect(move |_| {
let Some(node) = sentinel_ref.get() else { return };
let callback = Closure::<dyn Fn(Vec<js_sys::Object>)>::new(move |entries: Vec<js_sys::Object>| {
// entries.is_intersecting → call on_load_more
});
let observer = IntersectionObserver::new(callback.as_ref().unchecked_ref()).unwrap();
observer.observe(&node);
// cleanup: observer.disconnect() on on_cleanup
});
Step 1: Create message_feed.rs with grouping + sentinel + skeleton
Step 2: Verify compilation
Step 3: Commit
git commit -m "feat(leptos): MessageFeed with user grouping and infinite scroll"
Task 3: MessageCard — message rendering
Files:
- Create:
services/frontend-leptos/frontend/src/features/messages/components/message_card.rs
Interfaces:
- Consumes:
MessageRecord,api::messages::get_message_detail(for replied-to messages),ui::status_badge::StatusBadge - Produces:
MessageCardcomponent (props:messages: Vec<MessageRecord>,on_reanalyze: Callback<String>),MessageCardSkeletoncomponent
Key logic (ported from React MessageCard.tsx, 561 lines):
- Header: Avatar (img from
avatar_url), username, channel/thread info, time ago - Content: Render message with custom Discord emoji substitution (
<a:name:id>→ CDN<img>) - Edit/delete indicators: Pencil icon if
edited_atpresent, Trash2 icon ifdeleted_atpresent - Attachments: Images, videos, other files with download links
- Sticker: Display sticker image if metadata contains stickers
- Embeds: Show title + thumbnail from metadata embeds
- Reply context: If
metadata.channel.thread_idpresent, fetch the parent message - AI analysis box: StatusBadge, severity badge, confidence bar, categories, recommended action, error message, reanalyze button
Discord custom emoji regex:
use regex::Regex;
fn render_content(content: &str) -> Vec<HtmlElement> {
// <a:name:id> → animated GIF
// <:name:id> → static PNG
// URL: https://cdn.discordapp.com/emojis/{id}.{ext}?size=128
}
Note: regex crate needs to be added to Cargo.toml as a dependency.
Step 1: Add regex = "1" to frontend Cargo.toml
Step 2: Create message_card.rs with all sub-sections
Step 3: Verify compilation
Step 4: Commit
git commit -m "feat(leptos): MessageCard with Discord emoji, attachments, AI analysis display"
Task 4: ImageGrid
Files:
- Create:
services/frontend-leptos/frontend/src/features/messages/components/image_grid.rs
Interfaces:
- Consumes:
MessageRecord(extracts image URLs from attachments, stickers, embeds) - Produces:
ImageGridcomponent (props:messages: Vec<MessageRecord>)
Key logic (ported from React ImageGrid.tsx, 132 lines):
Extract images from message metadata:
fn extract_images(messages: &[MessageRecord]) -> Vec<String> {
let mut urls = Vec::new();
for msg in messages {
if let Some(meta) = &msg.metadata {
// attachments with image MIME
if let Some(atts) = &meta.attachments {
for att in atts {
if let Some(ct) = &att.content_type {
if ct.starts_with("image/") || ct.starts_with("video/") {
urls.push(att.url.clone());
}
}
}
}
// sticker images
if let Some(stickers) = &meta.stickers {
for s in stickers {
if let Some(url) = &s.url {
urls.push(url.clone());
}
}
}
// embed thumbnails/images
if let Some(embeds) = &meta.embeds {
for e in embeds {
if let Some(img) = &e.image { urls.push(img.url.clone()); }
if let Some(thumb) = &e.thumbnail { urls.push(thumb.url.clone()); }
}
}
}
}
urls.sort();
urls.dedup();
urls
}
Render as CSS grid of <img> elements with click-to-fullsize.
Step 1: Create image_grid.rs
Step 2: Verify compilation
Step 3: Commit
git commit -m "feat(leptos): ImageGrid component for message media"
Task 5: MessagesPanel — full composition
Files:
- Modify:
services/frontend-leptos/frontend/src/features/messages/mod.rs— replace placeholder with full panel - Modify:
services/frontend-leptos/frontend/src/app.rs— wireMessagesPanelas default tab content - Modify:
services/frontend-leptos/frontend/src/app.css— add message-specific CSS classes
Interfaces:
- Consumes:
use_messages::use_messages,MessageFeed,ImageGrid,ui::tabs::*,ui::badge::Badge,ui::input::Input,ui::button::Button,ui::card::*,app::UiContext,app::AuthContext,ws::context::WsContext
Key logic (ported from React MessagesPanel.tsx, 316 lines):
- State:
ai_filter,search_query,search_results,show_search,view_tab("all" | "images"),retrying_all,retried_count - Search: Debounced search via
api::messages::search_messages - Stats badges: total, clean, flagged, error, pending, deleted, edited
- AI filter chips: all, analyzed, clean, flagged, error, pending
- View tabs: All (MessageFeed) | Images (ImageGrid)
- WS integration:
message_created→ prepend,message_updated→ merge,message_deleted→ remove,message_analyzed→ update fields - Reanalyze: Individual + batch error reanalyze
CSS classes to add in app.css:
.filter-chip { /* pill-shaped filter button */ }
.filter-chip.active { /* active state */ }
.message-stats { /* flex row of stat badges */ }
.search-bar { /* search input with icon */ }
.messages-panel { /* main container */ }
Step 1: Create message-specific CSS
Step 2: Replace MessagesPanel with full implementation
Step 3: Wire into app.rs as default tab
Step 4: Add WS event handlers for real-time message updates
Step 5: Verify build
cd services/frontend-leptos/frontend && /home/asephs/.cargo/bin/trunk build --release
Step 6: Commit
git commit -m "feat(leptos): MessagesPanel with search, filters, real-time WS updates"
Task Dependency Graph
Task 1 (skeleton + hook)
├→ Task 2 (MessageFeed)
│ └→ Task 3 (MessageCard) ──┐
└→ Task 4 (ImageGrid) ─────────┤
└→ Task 5 (MessagesPanel composition)
- Tasks 2 and 4 can be dispatched in parallel (independent components)
- Task 3 depends on Task 2 (MessageFeed imports MessageCard)
- Task 5 depends on all of Tasks 2, 3, 4
CSS Classes Needed (append to app.css)
/* ── Messages Panel ──────────────────────────────────── */
.messages-panel {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.message-stats {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
}
.filter-chip {
padding: 0.25rem 0.75rem;
border-radius: var(--radius-full);
font-size: 0.75rem;
font-weight: 500;
border: 1px solid var(--surface-border);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.filter-chip:hover {
color: var(--text-primary);
background: var(--surface-overlay);
}
.filter-chip.active {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.search-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
}
.search-row .input {
flex: 1;
min-width: 200px;
}
/* ── Message Card ────────────────────────────────────── */
.message-card {
background: var(--surface-base);
border: 1px solid var(--surface-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.message-card-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-6);
border-bottom: 1px solid var(--surface-border);
}
.message-card-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.message-card-username {
font-weight: 600;
font-size: 0.9375rem;
}
.message-card-meta {
font-size: 0.75rem;
color: var(--text-tertiary);
}
.message-card-body {
padding: var(--space-4) var(--space-6);
}
.message-row {
padding: var(--space-2) 0;
border-bottom: 1px solid var(--surface-border);
}
.message-row:last-child {
border-bottom: none;
}
.message-timestamp {
font-size: 0.6875rem;
color: var(--text-tertiary);
min-width: 48px;
}
.message-content {
font-size: 0.875rem;
line-height: 1.5;
word-break: break-word;
}
.message-content .custom-emoji {
display: inline-block;
height: 22px;
width: 22px;
vertical-align: middle;
object-fit: contain;
}
/* ── AI Analysis Box ─────────────────────────────────── */
.ai-analysis-box {
margin-top: var(--space-2);
padding: var(--space-3);
background: var(--surface-overlay);
border-radius: var(--radius-md);
border: 1px solid var(--surface-border);
}
.ai-analysis-label {
font-size: 0.75rem;
color: var(--text-tertiary);
margin-bottom: var(--space-1);
}
.ai-analysis-text {
font-size: 0.8125rem;
line-height: 1.4;
}
.ai-confidence-bar {
height: 4px;
background: var(--surface-border);
border-radius: 2px;
margin-top: var(--space-2);
overflow: hidden;
}
.ai-confidence-fill {
height: 100%;
border-radius: 2px;
background: var(--color-primary);
transition: width var(--transition-normal);
}
/* ── Image Grid ──────────────────────────────────────── */
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--space-2);
}
.image-grid-item {
aspect-ratio: 1;
border-radius: var(--radius-md);
overflow: hidden;
cursor: pointer;
transition: opacity var(--transition-fast);
}
.image-grid-item:hover {
opacity: 0.85;
}
.image-grid-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
Key Reference Files
- React MessagesPanel:
services/frontend/src/features/messages/index.tsx(316 lines) - React useMessages:
services/frontend/src/features/messages/hooks/useMessages.ts(162 lines) - React MessageFeed:
services/frontend/src/features/messages/components/MessageFeed.tsx(120 lines) - React MessageCard:
services/frontend/src/features/messages/components/MessageCard.tsx(561 lines) - React ImageGrid:
services/frontend/src/features/messages/components/ImageGrid.tsx(132 lines) - Leptos shared-types:
services/frontend-leptos/shared-types/src/message.rs - Leptos API client:
services/frontend-leptos/frontend/src/api/messages.rs - Leptos WS context:
services/frontend-leptos/frontend/src/ws/context.rs - Leptos app shell:
services/frontend-leptos/frontend/src/app.rs