feat(automod): render sticker, role & user names in moderation views

QoL lanjutan dari fix60084b3: content pesan mentah masih nampilin
snowflake (<@&roleid>, <@userid>, <:emoji:id>) di log moderasi dan
prompt LLM. Sekarang dirender ke nama yang bisa dibaca:

- Gateway capture: metadata menyimpan mentionedRoles + mentionedUsers
  (id+name) dari message.mentions, disimpan ke metadata JSON
- renderDiscordMentions(): <@&id> -> @RoleName, <@id> -> @Username,
  <:name:id> -> :name:, fallback @role/@user — dipakai di
  conversationContext (konteks LLM) dan moderationBuilders
  (getAnalysisContent) sehingga LLM lihat nama role/user beneran,
  bukan placeholder generik
- Frontend renderMessageContent() (mirror gateway) dipasang di semua
  tempat nampilin content: message-card, message-detail(-view),
  search-overlay, search-panel, users/channels section, live-stream,
  mod-queue, review list; sticker-only message tetap [Sticker: name],
  pesan teks+sticker kini ikut nampilin nama sticker
- tsc --noEmit PASS di gateway & frontend; renderDiscordMentions
  diverifikasi manual (6 kasus: role/user/emoji/unknown/plain)
This commit is contained in:
asepharyana
2026-08-01 08:56:10 +07:00
parent 6df4f306dd
commit 1f91f99de3
16 changed files with 182 additions and 18 deletions
@@ -13,7 +13,7 @@ import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { useReanalyze } from "@/hooks";
import { messagesApi } from "@/lib/api";
import { safeParseJsonArray } from "@/lib/format";
import { renderMessageContent, safeParseJsonArray } from "@/lib/format";
import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils";
@@ -100,7 +100,7 @@ export function SearchPanel() {
</Badge>
)}
</div>
<p className="text-sm leading-relaxed">{msg.content}</p>
<p className="text-sm leading-relaxed">{renderMessageContent(msg.content, msg.metadata)}</p>
{msg.ai_moderation_flags &&
msg.ai_moderation_flags !== "[]" && (
<div className="flex flex-wrap gap-1">
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useChannelDetail, useChannels } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type { DashboardChannel } from "@/lib/types";
export function ChannelsSection({ guildId }: { guildId?: string }) {
@@ -123,7 +124,7 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
className="rounded-lg border border-border/40 bg-card/40 px-3 py-2"
>
<p className="text-xs leading-relaxed text-text-secondary line-clamp-2">
{msg.username}: {msg.content || "(no text content)"}
{msg.username}: {renderMessageContent(msg.content, msg.metadata) || "(no text content)"}
</p>
<p className="mt-1 text-[10px] font-mono text-text-secondary/40">
{new Date(msg.created_at).toLocaleString()}
@@ -2,6 +2,7 @@
import { useEffect, useRef, useState } from "react";
import { GlassCard } from "@/components/glass/card";
import { renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils";
import { useWebSocket } from "@/lib/ws/context";
@@ -13,6 +14,7 @@ interface LiveMessage {
channelName?: string;
timestamp: string;
flagged?: boolean;
metadata?: string | null;
}
export function LiveStream() {
@@ -40,6 +42,7 @@ export function LiveStream() {
channelName,
timestamp: new Date().toLocaleTimeString(),
flagged: data.ai_status === "flagged" || data.ai_status === "warn",
metadata: data.metadata ?? null,
};
setMessages((prev) => [msg, ...prev].slice(0, 50));
});
@@ -80,7 +83,7 @@ export function LiveStream() {
{msg.username}
</span>
<span className="text-xs text-text-secondary truncate flex-1">
{msg.content}
{renderMessageContent(msg.content, msg.metadata)}
</span>
<span className="text-[10px] text-text-secondary/40 shrink-0">
{msg.timestamp}
@@ -2,6 +2,7 @@
import { AlertCircle, Check, Trash2 } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { renderMessageContent } from "@/lib/format";
import { cn } from "@/lib/utils";
export interface ModQueueItem {
@@ -10,6 +11,7 @@ export interface ModQueueItem {
username: string;
severity: "low" | "medium" | "high" | "critical";
reason: string;
metadata?: string | null;
}
export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) {
@@ -56,7 +58,7 @@ export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) {
</span>
</div>
<p className="text-xs text-text-secondary line-clamp-1">
{item.content}
{renderMessageContent(item.content, item.metadata)}
</p>
<p className="text-[10px] text-text-secondary/50">
{item.reason}
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useUserDetail, useUsers } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type { DashboardUser } from "@/lib/types";
export function UsersSection() {
@@ -134,7 +135,7 @@ export function UsersSection() {
className="rounded-lg border border-border/40 bg-card/40 px-3 py-2"
>
<p className="text-xs leading-relaxed text-text-secondary line-clamp-2">
{msg.content || "(no text content)"}
{renderMessageContent(msg.content, msg.metadata) || "(no text content)"}
</p>
<p className="mt-1 text-[10px] font-mono text-text-secondary/40">
{msg.channel_id?.slice(0, 8)} ·{" "}
@@ -6,7 +6,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { safeParseJsonArray, getMessageChannelLabel } from "@/lib/format";
import { safeParseJsonArray, getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils";
import { AiStatusBadge } from "./ai-status-badge";
@@ -96,7 +96,7 @@ export function MessageCard({
"italic text-muted-foreground line-through",
)}
>
{msg.content}
{renderMessageContent(msg.content, msg.metadata)}
</p>
{(() => {
const u = extractFirstImage(msg.metadata);
@@ -4,7 +4,7 @@ import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { AttachmentsGrid } from "./attachments-grid";
import { AiAnalysisPanel } from "./ai-analysis-panel";
import { getMessageChannelLabel } from "@/lib/format";
import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
interface MessageDetailViewProps {
@@ -34,7 +34,7 @@ export function MessageDetailView({ message, attachments, onBack }: MessageDetai
{/* Content */}
<div className="text-sm text-text-primary/90 leading-relaxed mb-4 whitespace-pre-wrap">
{message.content || "(no text content)"}
{renderMessageContent(message.content, message.metadata) || "(no text content)"}
</div>
{/* Attachments */}
@@ -4,7 +4,7 @@ import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
import { GlassCard } from "@/components/glass/card";
import { AttachmentsGrid } from "./attachments-grid";
import { AiAnalysisPanel } from "./ai-analysis-panel";
import { getMessageChannelLabel } from "@/lib/format";
import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
interface MessageDetailProps {
@@ -34,7 +34,7 @@ export function MessageDetail({ message, attachments, onBack }: MessageDetailPro
{/* Content */}
<div className="text-sm text-text-primary/90 leading-relaxed mb-4 whitespace-pre-wrap">
{message.content || "(no text content)"}
{renderMessageContent(message.content, message.metadata) || "(no text content)"}
</div>
{/* Attachments */}
@@ -4,7 +4,7 @@ import { Search, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { messagesApi } from "@/lib/api";
import { getMessageChannelLabel } from "@/lib/format";
import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types";
interface SearchOverlayProps {
@@ -86,7 +86,7 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
<span className="font-medium text-text-primary">{msg.username}</span>
<span className="text-text-secondary/40">{getMessageChannelLabel(msg)}</span>
</div>
<p className="text-xs text-text-secondary/80 line-clamp-1 mt-0.5">{msg.content}</p>
<p className="text-xs text-text-secondary/80 line-clamp-1 mt-0.5">{renderMessageContent(msg.content, msg.metadata)}</p>
</button>
))
)}