import { useState, useMemo, Fragment } from "react"; import type { MessageRecord } from "../../../shared/api/client"; import { Badge, Button, Skeleton } from "../../../shared/ui"; import { RotateCw, AlertCircle, CheckCircle2, AlertTriangle, Trash2, Pencil, Image as ImageIcon, Smile } from "lucide-react"; const CUSTOM_EMOJI_REGEX = /<(a)?:([a-zA-Z0-9_]+):(\d+)>/g; /** * Renders message content with Discord custom emojis displayed as images * instead of raw text like `<:name:id>`. */ function renderContentWithCustomEmojis(content: string): React.ReactNode { const parts: React.ReactNode[] = []; const regex = new RegExp(CUSTOM_EMOJI_REGEX.source, "g"); let lastIndex = 0; let match: RegExpExecArray | null; while ((match = regex.exec(content)) !== null) { // Text before the emoji if (match.index > lastIndex) { parts.push(content.slice(lastIndex, match.index)); } const [, animated, name, id] = match; const ext = animated ? "gif" : "png"; const url = `https://cdn.discordapp.com/emojis/${id}.${ext}?size=128`; parts.push( {name}, ); lastIndex = regex.lastIndex; } // Remaining text after last emoji if (lastIndex < content.length) { parts.push(content.slice(lastIndex)); } // If no emojis were found, just return the raw content if (parts.length === 0) { return content; } return {parts}; } interface MessageCardProps { message: MessageRecord; onReanalyze: (id: string) => Promise; } interface MessageMetadata { stickers?: Array<{ name?: string; url?: string }>; attachments?: Array<{ name: string; url: string; contentType?: string }>; embeds?: Array<{ title?: string; image?: string; thumbnail?: string }>; } function parseMetadata(value: string | null): MessageMetadata { if (!value) return {}; try { return JSON.parse(value) as MessageMetadata; } catch { return {}; } } function parseStringList(value?: string | null): string[] { if (!value) return []; try { const parsed = JSON.parse(value) as unknown; return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : []; } catch { return value.split(",").map((item) => item.trim()).filter(Boolean); } } function aiVariant(status: string) { if (status === "clean") return "success"; if (status === "warn") return "warning"; if (status === "flagged" || status === "error") return "destructive"; return "secondary"; } function severityColor(severity: string) { switch (severity) { case "critical": return "bg-red-500/20 text-red-300 border-red-500/30"; case "high": return "bg-orange-500/20 text-orange-300 border-orange-500/30"; case "medium": return "bg-yellow-500/20 text-yellow-300 border-yellow-500/30"; case "low": return "bg-blue-500/20 text-blue-300 border-blue-500/30"; default: return "bg-muted text-muted-foreground border-border"; } } function formatTimeAgo(ts: number): string { const seconds = Math.floor((Date.now() - ts) / 1000); if (seconds < 60) return `${seconds}s ago`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; return new Date(ts).toLocaleDateString(); } export function MessageCard({ message, onReanalyze }: MessageCardProps) { const metadata = useMemo(() => parseMetadata(message.metadata), [message.metadata]); const displayContent = message.edited_content ?? message.content; const aiStatus = message.ai_status ?? "pending"; const categories = useMemo(() => { const list = parseStringList(message.ai_categories ?? message.ai_moderation_flags); return list.filter((c) => c !== "analysis_incomplete"); }, [message.ai_categories, message.ai_moderation_flags]); const confidence = message.ai_confidence ?? message.ai_moderation_score ?? null; const [isReanalyzing, setIsReanalyzing] = useState(false); const stickers = metadata.stickers ?? []; const attachments = metadata.attachments ?? []; const imageAttachments = attachments.filter( (a) => a.contentType?.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(a.name), ); const hasImages = imageAttachments.length > 0; const handleReanalyze = async () => { setIsReanalyzing(true); try { await onReanalyze(message.id); } finally { setIsReanalyzing(false); } }; return (
{message.username || message.user_id} {formatTimeAgo(message.created_at)} {message.edited_at && ( edited )} {message.deleted_at && ( deleted )}
{aiStatus === "clean" && } {aiStatus === "warn" && } {aiStatus === "flagged" && } {aiStatus === "error" && } {aiStatus} {message.ai_severity && message.ai_severity !== "none" && ( {message.ai_severity} )} {confidence != null && ( {Math.round(confidence * 100)}% )}
{displayContent ? (

{renderContentWithCustomEmojis(displayContent)}

) : null} {stickers.length > 0 && (
{stickers.map((sticker) => (
{sticker.url ? ( {sticker.name ) : (
)} {sticker.name}
))}
)} {hasImages && (
{imageAttachments.slice(0, 4).map((img) => ( {img.name} ))} {imageAttachments.length > 4 && (
+{imageAttachments.length - 4}
)}
)} {categories.length > 0 && (
{categories.map((category) => ( {category} ))}
)} {message.ai_analysis ? (
{message.ai_analysis}
) : null} {message.ai_error ? (
AI error: {message.ai_error}
) : null}
{aiStatus === "error" && ( Click to retry analysis )}
); } export function MessageCardSkeleton() { return (
); }