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:
@@ -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[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user