refactor(frontend): remove warn moderation status — hardcode warn to 0 in charts, drop warn filter/badge/buttons/Api, simplify to flagged-only

This commit is contained in:
MythEclipse
2026-06-03 01:13:47 +07:00
parent b7d4e66600
commit 8d34aa7125
10 changed files with 15 additions and 108 deletions
+3 -3
View File
@@ -97,15 +97,15 @@ export default function App() {
onMessageAnalyzed: (m) => { onMessageAnalyzed: (m) => {
const msg = m as MessageRecord; const msg = m as MessageRecord;
messages.setMessages((prev) => mergeMessages(prev, [msg])); messages.setMessages((prev) => mergeMessages(prev, [msg]));
// Show toast for moderation alerts (warn/flagged) // Show toast for moderation alerts (flagged)
const status = msg.ai_status; const status = msg.ai_status;
if (status === "flagged" || status === "warn") { if (status === "flagged") {
const username = msg.username || msg.user_id || "unknown"; const username = msg.username || msg.user_id || "unknown";
const severity = msg.ai_severity || ""; const severity = msg.ai_severity || "";
const categories = msg.ai_categories || ""; const categories = msg.ai_categories || "";
const brief = const brief =
msg.ai_analysis?.slice(0, 80) ?? msg.ai_analysis?.slice(0, 80) ??
`Message ${status === "flagged" ? "flagged" : "warned"} by AI`; "Message flagged by AI";
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("moderation_alert", { new CustomEvent("moderation_alert", {
detail: { type: status, username, severity, categories, brief }, detail: { type: status, username, severity, categories, brief },
@@ -27,7 +27,7 @@ export function ActivityChart({ hourly, loading }: ActivityChartProps) {
return { return {
hour: `${String(jakartaHour).padStart(2, "0")}:00`, hour: `${String(jakartaHour).padStart(2, "0")}:00`,
clean: b.clean, clean: b.clean,
warned: b.warned, warned: 0,
flagged: b.flagged, flagged: b.flagged,
error: b.error, error: b.error,
total: b.count, total: b.count,
@@ -22,10 +22,6 @@ export function SummaryCards({
messages && messages.total > 0 messages && messages.total > 0
? Math.round((messages.clean / messages.total) * 100) ? Math.round((messages.clean / messages.total) * 100)
: 0; : 0;
const warnedPct =
messages && messages.total > 0
? Math.round((messages.warned / messages.total) * 100)
: 0;
const flaggedPct = const flaggedPct =
messages && messages.total > 0 messages && messages.total > 0
? Math.round((messages.flagged / messages.total) * 100) ? Math.round((messages.flagged / messages.total) * 100)
@@ -47,11 +43,6 @@ export function SummaryCards({
value: cleanPct > 0 ? `${cleanPct}%` : "—", value: cleanPct > 0 ? `${cleanPct}%` : "—",
accent: "text-emerald-400", accent: "text-emerald-400",
}, },
{
label: "Warned",
value: warnedPct > 0 ? `${warnedPct}%` : "—",
accent: "text-amber-400",
},
{ {
label: "Flagged", label: "Flagged",
value: flaggedPct > 0 ? `${flaggedPct}%` : "—", value: flaggedPct > 0 ? `${flaggedPct}%` : "—",
@@ -24,7 +24,7 @@ export function TrendChart({ trend, loading }: TrendChartProps) {
const data = trend.map((bucket) => ({ const data = trend.map((bucket) => ({
date: bucket.date, date: bucket.date,
clean: bucket.clean, clean: bucket.clean,
warned: bucket.warned, warned: 0,
flagged: bucket.flagged, flagged: bucket.flagged,
error: bucket.error, error: bucket.error,
total: bucket.count, total: bucket.count,
@@ -53,7 +53,6 @@ export function UserTable({ users, loading }: UserTableProps) {
<th className="py-2 pr-2 font-semibold">User</th> <th className="py-2 pr-2 font-semibold">User</th>
<th className="py-2 pr-2 font-semibold text-right">Pesan</th> <th className="py-2 pr-2 font-semibold text-right">Pesan</th>
<th className="py-2 pr-2 font-semibold text-right">Edit</th> <th className="py-2 pr-2 font-semibold text-right">Edit</th>
<th className="py-2 pr-2 font-semibold text-right">Hapus</th>
<th className="py-2 pr-4 font-semibold text-right">Flag</th> <th className="py-2 pr-4 font-semibold text-right">Flag</th>
</tr> </tr>
</thead> </thead>
@@ -103,9 +102,6 @@ export function UserTable({ users, loading }: UserTableProps) {
<td className="py-1.5 pr-2 text-right font-mono text-[10px] text-muted-foreground tabular-nums"> <td className="py-1.5 pr-2 text-right font-mono text-[10px] text-muted-foreground tabular-nums">
{user.edited_count > 0 ? user.edited_count : "—"} {user.edited_count > 0 ? user.edited_count : "—"}
</td> </td>
<td className="py-1.5 pr-2 text-right font-mono text-[10px] text-muted-foreground tabular-nums">
{user.deleted_count > 0 ? user.deleted_count : "—"}
</td>
<td className="py-1.5 pr-4 text-right"> <td className="py-1.5 pr-4 text-right">
{user.flagged_count > 0 ? ( {user.flagged_count > 0 ? (
<Badge <Badge
@@ -48,7 +48,7 @@ export function ViolatorTable({ users, loading }: ViolatorTableProps) {
Pelanggar Terbanyak Pelanggar Terbanyak
</CardTitle> </CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
Skor: flagged × 3 + warned × 1. Skor: flagged × 3.
</CardDescription> </CardDescription>
</div> </div>
<Badge variant="destructive">{users.length} pelanggar</Badge> <Badge variant="destructive">{users.length} pelanggar</Badge>
@@ -61,7 +61,6 @@ export function ViolatorTable({ users, loading }: ViolatorTableProps) {
<tr className="sticky top-0 z-10 bg-card/95 backdrop-blur border-b border-border text-left text-[10px] uppercase tracking-wider text-muted-foreground"> <tr className="sticky top-0 z-10 bg-card/95 backdrop-blur border-b border-border text-left text-[10px] uppercase tracking-wider text-muted-foreground">
<th className="py-2 pl-4 pr-2 font-semibold">#</th> <th className="py-2 pl-4 pr-2 font-semibold">#</th>
<th className="py-2 pr-2 font-semibold">User</th> <th className="py-2 pr-2 font-semibold">User</th>
<th className="py-2 pr-2 font-semibold text-right">Warned</th>
<th className="py-2 pr-2 font-semibold text-right">Flagged</th> <th className="py-2 pr-2 font-semibold text-right">Flagged</th>
<th className="py-2 pr-4 font-semibold text-right">Skor</th> <th className="py-2 pr-4 font-semibold text-right">Skor</th>
</tr> </tr>
@@ -102,9 +101,6 @@ export function ViolatorTable({ users, loading }: ViolatorTableProps) {
</Badge> </Badge>
</div> </div>
</td> </td>
<td className="py-1.5 pr-2 text-right font-mono text-xs text-amber-400 tabular-nums">
{user.warned_count}
</td>
<td className="py-1.5 pr-2 text-right font-mono text-xs text-red-400 tabular-nums"> <td className="py-1.5 pr-2 text-right font-mono text-xs text-red-400 tabular-nums">
{user.flagged_count} {user.flagged_count}
</td> </td>
@@ -1,6 +1,5 @@
import { import {
AlertCircle, AlertCircle,
AlertTriangle,
CheckCircle2, CheckCircle2,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@@ -12,7 +11,6 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Fragment, useMemo, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import type { MessageRecord } from "../../../shared/api/client"; import type { MessageRecord } from "../../../shared/api/client";
import { moderateMessage } from "../../../shared/api/client";
import { Badge, Button, Skeleton } from "../../../shared/ui"; import { Badge, Button, Skeleton } from "../../../shared/ui";
import { parseMetadata } from "../../../entities/message/types"; import { parseMetadata } from "../../../entities/message/types";
@@ -89,7 +87,6 @@ function parseStringList(value?: string | null): string[] {
function aiVariant(status: string) { function aiVariant(status: string) {
if (status === "clean") return "success"; if (status === "clean") return "success";
if (status === "warn") return "warning";
if (status === "flagged" || status === "error") return "destructive"; if (status === "flagged" || status === "error") return "destructive";
return "secondary"; return "secondary";
} }
@@ -138,7 +135,7 @@ export function MessageCard({
message.ai_confidence ?? message.ai_moderation_score ?? null; message.ai_confidence ?? message.ai_moderation_score ?? null;
const [isReanalyzing, setIsReanalyzing] = useState(false); const [isReanalyzing, setIsReanalyzing] = useState(false);
const [showAnalysis, setShowAnalysis] = useState( const [showAnalysis, setShowAnalysis] = useState(
aiStatus === "warn" || aiStatus === "flagged", aiStatus === "flagged",
); );
// Build a human-readable analysis summary from categories + confidence + severity // Build a human-readable analysis summary from categories + confidence + severity
@@ -223,9 +220,6 @@ export function MessageCard({
{aiStatus === "clean" && ( {aiStatus === "clean" && (
<CheckCircle2 className="h-3.5 w-3.5" /> <CheckCircle2 className="h-3.5 w-3.5" />
)} )}
{aiStatus === "warn" && (
<AlertTriangle className="h-3.5 w-3.5" />
)}
{aiStatus === "flagged" && ( {aiStatus === "flagged" && (
<AlertCircle className="h-3.5 w-3.5" /> <AlertCircle className="h-3.5 w-3.5" />
)} )}
@@ -328,9 +322,7 @@ export function MessageCard({
className={`rounded-xl border-l-2 p-3 ${ className={`rounded-xl border-l-2 p-3 ${
aiStatus === "flagged" aiStatus === "flagged"
? "border-l-red-500 bg-red-500/5" ? "border-l-red-500 bg-red-500/5"
: aiStatus === "warn" : "border-l-blue-500 bg-blue-500/5"
? "border-l-yellow-500 bg-yellow-500/5"
: "border-l-blue-500 bg-blue-500/5"
}`} }`}
> >
<button <button
@@ -341,9 +333,7 @@ export function MessageCard({
<span className="font-medium text-foreground/80"> <span className="font-medium text-foreground/80">
{aiStatus === "flagged" {aiStatus === "flagged"
? "🚨" ? "🚨"
: aiStatus === "warn" : "️"}{" "}
? "⚠️"
: "️"}{" "}
{analysisSummary} {analysisSummary}
</span> </span>
{showAnalysis ? ( {showAnalysis ? (
@@ -384,52 +374,6 @@ export function MessageCard({
Click to retry analysis Click to retry analysis
</span> </span>
)} )}
{/* Moderation action buttons for flagged/warned messages */}
{(aiStatus === "flagged" || aiStatus === "warn") && (
<div className="flex items-center gap-1.5 border-l border-border pl-2">
<Button
size="sm"
variant="destructive"
className="text-xs"
onClick={() => {
if (
window.confirm(
`Delete message from ${message.username}?\n\n"${(message.edited_content ?? message.content).slice(0, 120)}"`,
)
) {
moderateMessage(
message.id,
"delete_message",
"Manual moderation from dashboard",
);
}
}}
>
<Trash2 className="h-3 w-3 mr-1" /> Delete
</Button>
<Button
size="sm"
variant="outline"
className="text-xs"
onClick={() => {
if (
window.confirm(
`Warn user ${message.username} for this message?`,
)
) {
moderateMessage(
message.id,
"warn_user",
"Manual moderation from dashboard",
);
}
}}
>
<AlertTriangle className="h-3 w-3 mr-1" /> Warn
</Button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -1,12 +1,12 @@
// ─── Moderation alert toast listener ─────────────────────────────────────── // ─── Moderation alert toast listener ───────────────────────────────────────
// Listens for "moderation_alert" custom events dispatched from WebSocket // Listens for "moderation_alert" custom events dispatched from WebSocket
// message_analyzed handler, and shows toast notifications for flagged/warned // message_analyzed handler, and shows toast notifications for flagged
// messages so moderators don't miss important alerts. // messages so moderators don't miss important alerts.
import { useEffect } from "react"; import { useEffect } from "react";
import { useToast } from "../../../shared/ui"; import { useToast } from "../../../shared/ui";
interface AlertDetail { interface AlertDetail {
type: "flagged" | "warn"; type: "flagged";
username: string; username: string;
severity: string; severity: string;
categories: string; categories: string;
@@ -18,19 +18,18 @@ export function ModerationAlertListener() {
useEffect(() => { useEffect(() => {
const handler = (e: Event) => { const handler = (e: Event) => {
const { type, username, severity, categories, brief } = ( const { username, severity, categories, brief } = (
e as CustomEvent<AlertDetail> e as CustomEvent<AlertDetail>
).detail; ).detail;
const emoji = type === "flagged" ? "🚨" : "⚠️";
const sevLabel = severity ? `[${severity}]` : ""; const sevLabel = severity ? `[${severity}]` : "";
const catLabel = categories const catLabel = categories
? `${categories.split(",").slice(0, 2).join(", ")}` ? `${categories.split(",").slice(0, 2).join(", ")}`
: ""; : "";
addToast( addToast(
`${emoji} ${username} ${sevLabel}${catLabel}: ${brief}`, `🚨 ${username} ${sevLabel}${catLabel}: ${brief}`,
type === "flagged" ? "error" : "warning", "error",
); );
}; };
@@ -27,7 +27,7 @@ interface MessagesPanelProps {
loadingMore?: boolean; loadingMore?: boolean;
} }
type AiFilter = "all" | "clean" | "warn" | "flagged" | "error" | "pending"; type AiFilter = "all" | "clean" | "flagged" | "error" | "pending";
export function MessagesPanel({ export function MessagesPanel({
guildName, guildName,
@@ -73,7 +73,6 @@ export function MessagesPanel({
return { return {
total: base.length, total: base.length,
clean: base.filter((m) => m.ai_status === "clean").length, clean: base.filter((m) => m.ai_status === "clean").length,
warn: base.filter((m) => m.ai_status === "warn").length,
flagged: base.filter((m) => m.ai_status === "flagged").length, flagged: base.filter((m) => m.ai_status === "flagged").length,
error: base.filter((m) => m.ai_status === "error").length, error: base.filter((m) => m.ai_status === "error").length,
pending: base.filter((m) => m.ai_status === "pending" || !m.ai_status) pending: base.filter((m) => m.ai_status === "pending" || !m.ai_status)
@@ -125,12 +124,6 @@ export function MessagesPanel({
> >
{stats.clean} clean {stats.clean} clean
</Badge> </Badge>
<Badge
variant="outline"
className="text-xs text-yellow-400 border-yellow-400/30"
>
{stats.warn} warn
</Badge>
<Badge <Badge
variant="outline" variant="outline"
className="text-xs text-red-400 border-red-400/30" className="text-xs text-red-400 border-red-400/30"
@@ -227,7 +220,6 @@ export function MessagesPanel({
[ [
"all", "all",
"clean", "clean",
"warn",
"flagged", "flagged",
"error", "error",
"pending", "pending",
@@ -178,17 +178,6 @@ export function reanalyzeErrorBatch(opts: {
); );
} }
export function moderateMessage(
id: string,
actionType: string,
reason?: string,
): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/api/messages/${id}/moderate`, {
method: "POST",
body: JSON.stringify({ actionType, reason }),
});
}
// ─── Guilds / Config ───────────────────────────────────────────────────────── // ─── Guilds / Config ─────────────────────────────────────────────────────────
export function getGuilds(): Promise<Guild[]> { export function getGuilds(): Promise<Guild[]> {