feat: comprehensive UX improvements — messages, analysis, moderation
Fix #1 (CRITICAL): Wire message_analyzed through Redis EventBroadcaster - DG aiAnalyzer.ts: Add broadcastAnalysisCompleted() helper that publishes to both in-memory WS broadcaster AND Redis EventBroadcaster - DG bootstrap.ts: Pass eventBroadcaster to startPendingAIAnalysisWorker() - Fixes broken real-time chain so analysis results appear instantly Fix #2: Collapsible AI Analysis with Rich Formatting - MessageCard: AI analysis now collapsible with color-coded severity border (red/yellow/blue), summary line showing categories + confidence + severity - Default collapsed for clean, expanded for warn/flagged Fix #3: Toast Notifications for Flagged Content - Wrap app in ToastProvider; ModerationAlertListener component listens for moderation_alert custom events and shows toast with emoji + details Fix #4: Discord-style Message Grouping - MessageFeed: Group consecutive messages from same user within 5 min - MessageCard: Compact variant hides avatar, reduces padding for non-first Fix #5: Moderation Action Buttons in UI - BE: POST /api/messages/:id/moderate endpoint + publishCommand() for Redis - FE: Delete/Warn buttons on flagged/warned cards with confirmation dialog Fix #6: Search Results Include Full Data - BE analysis.service.ts: SELECT all 26 message columns instead of just 11 - Search results now render with full MessageCard including images/analysis Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
74400376a0
commit
c5c667b147
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import type { MessageRecord } from "../../../shared/api/client";
|
||||
import { ScrollArea } from "../../../shared/ui";
|
||||
import { MessageCard, MessageCardSkeleton } from "./MessageCard";
|
||||
@@ -13,6 +13,32 @@ export interface MessageFeedProps {
|
||||
loadingMore?: boolean;
|
||||
}
|
||||
|
||||
/** Messages from the same user within 5 minutes are visually grouped. */
|
||||
const GROUP_WINDOW_MS = 5 * 60 * 1000;
|
||||
|
||||
interface MessageGroup {
|
||||
messages: MessageRecord[];
|
||||
}
|
||||
|
||||
function groupMessages(messages: MessageRecord[]): MessageGroup[] {
|
||||
const groups: MessageGroup[] = [];
|
||||
for (const msg of messages) {
|
||||
const lastGroup = groups[groups.length - 1];
|
||||
if (
|
||||
lastGroup &&
|
||||
lastGroup.messages[0].user_id === msg.user_id &&
|
||||
lastGroup.messages[lastGroup.messages.length - 1].created_at -
|
||||
msg.created_at <
|
||||
GROUP_WINDOW_MS
|
||||
) {
|
||||
lastGroup.messages.push(msg);
|
||||
} else {
|
||||
groups.push({ messages: [msg] });
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function MessageFeed({
|
||||
messages,
|
||||
onReanalyze,
|
||||
@@ -40,6 +66,8 @@ export function MessageFeed({
|
||||
return () => observer.disconnect();
|
||||
}, [onLoadMore, hasMore]);
|
||||
|
||||
const groupedMessages = useMemo(() => groupMessages(messages), [messages]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
||||
@@ -63,13 +91,20 @@ export function MessageFeed({
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-260px)] pr-3">
|
||||
<div className="space-y-3">
|
||||
{messages.map((message) => (
|
||||
<MessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
onReanalyze={onReanalyze}
|
||||
/>
|
||||
))}
|
||||
{groupedMessages.map((group) =>
|
||||
group.messages.map((message, idx) => {
|
||||
const isFirstInGroup = idx === 0;
|
||||
const isCompact = !isFirstInGroup;
|
||||
return (
|
||||
<MessageCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
onReanalyze={onReanalyze}
|
||||
compact={isCompact}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
|
||||
{/* Infinite-scroll sentinel */}
|
||||
{hasMore && (
|
||||
|
||||
Reference in New Issue
Block a user