Files
GMW/services/frontend/src/app/(dashboard)/moderation/view.tsx
T
asepharyana 38b74cc1db feat(fe): enrich dashboard, moderation, messages inspector, channels roster with full backend data
- Dashboard: 6-tile metric deck (flagged today, active users 24h, mod queue),
  pipeline status bar, top channels ranking, enriched reactions with emojis,
  warned counter in gauge, 4 quick-nav links
- Moderation: coverage tile + header rate, top flagged domains, top flagged
  channels, category/severity/action breakdown trends, coverage progress bar
- Messages inspector: severity meter, confidence %, risk score, recommended
  action badge, reply-context chain, mention + role chips
- Channels: roster/culture tabs, rich table (messages, clean, flagged, risk
  bar, last activity) wired to dashboard.channels endpoint
2026-09-01 23:51:39 +07:00

381 lines
14 KiB
TypeScript

"use client";
import {
AlertTriangle,
BarChart3,
CheckCircle2,
Globe,
Hash,
Shield,
TrendingUp,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useAmbient } from "@/components/ambient/ambient-context";
import { LiveModerationFeed } from "@/components/LiveModerationFeed";
import { ModerationHeatmap } from "@/components/ModerationHeatmap";
import { GlassPanel } from "@/components/primitives";
import { ScamDomains } from "@/components/ScamDomains";
import {
ErrorState,
MetricTile,
PageTransition,
SectionHeader,
SkeletonMetricRow,
SkeletonPanel,
} from "@/components/shared";
import { TopChannels } from "@/components/TopChannels";
import {
useHourlyModeration,
useModerationActions,
useModerationCoverage,
useModerationStats,
useModerationTrends,
useTopFlaggedChannels,
useTopFlaggedDomains,
} from "@/hooks";
import { useStaggerReveal } from "@/hooks/use-gsap-animation";
import { formatNumber } from "@/lib/format";
import type {
ModerationAction,
ModerationCoverage,
ModerationStats,
} from "@/lib/types";
export function ModerationView({
initialStats,
initialActions,
initialCoverage: _initialCoverage,
}: {
initialStats?: ModerationStats;
initialActions?: ModerationAction[];
initialCoverage?: ModerationCoverage;
}) {
const {
data: stats,
isLoading,
error,
mutate,
} = useModerationStats(initialStats);
const { data: actions } = useModerationActions(
undefined,
undefined,
initialActions,
);
const { data: hourly } = useHourlyModeration();
const { data: domains } = useTopFlaggedDomains(30);
const { data: channels } = useTopFlaggedChannels(30);
const { data: trends } = useModerationTrends(30);
const { data: coverage } = useModerationCoverage(30);
const ambient = useAmbient();
const [filterMode, setFilterMode] = useState<"all" | "flagged" | "clean">(
"all",
);
const containerRef = useStaggerReveal<HTMLDivElement>(".mod-tile", {
stagger: 0.035,
y: 8,
dependencies: [stats],
});
const executed = stats?.executed ?? 0;
const failed = stats?.failed ?? 0;
const total = stats?.total || 1;
const failedRatio = failed / total;
useEffect(() => {
if (failedRatio > 0.15) ambient.set("vermilion", 0.5, "high failure rate");
else if (failedRatio > 0.05) ambient.set("amber", 0.35, "moderate issues");
else ambient.set("signal", 0.25, "moderation nominal");
}, [failedRatio, ambient]);
if (error && !stats)
return <ErrorState error={error} onRetry={() => void mutate()} />;
if (!stats && isLoading)
return (
<div className="space-y-4">
<SkeletonMetricRow cols={3} />
<SkeletonPanel rows={6} />
</div>
);
if (!stats) return null;
return (
<PageTransition>
<div ref={containerRef} className="space-y-4">
{/* Tactical HUD Header Bar */}
<div className="mod-tile flex flex-wrap items-center justify-between gap-3 border-b border-hairline pb-3">
<div className="flex items-center gap-2.5">
<span
className={`h-2 w-2 rounded-full ${
failedRatio > 0.1
? "bg-vermilion shadow-[0_0_8px_var(--color-vermilion-glow)]"
: "bg-signal shadow-[0_0_8px_var(--color-signal-glow)]"
}`}
/>
<h1 className="font-mono text-xs font-semibold tracking-wide text-ink uppercase">
Moderation Intelligence Console
</h1>
</div>
<div className="flex items-center gap-2 font-mono text-[11px] text-ink-muted">
<span>EXECUTION:</span>
<span className="rounded bg-signal/15 px-2 py-0.5 font-medium text-signal border border-signal/30">
{Math.round((executed / total) * 100)}% RATE
</span>
{coverage && (
<>
<span>COVERAGE:</span>
<span
className={`rounded px-2 py-0.5 font-medium border ${
coverage.coverage_rate >= 90
? "bg-signal/15 text-signal border-signal/30"
: "bg-amber/15 text-amber border-amber/30"
}`}
>
{Math.round(coverage.coverage_rate)}%
</span>
</>
)}
</div>
<div className="flex items-center gap-1.5 rounded-[6px] border border-hairline bg-surface-2 p-0.5">
{(["all", "flagged", "clean"] as const).map((mode) => (
<button
key={mode}
type="button"
onClick={() => setFilterMode(mode)}
className={`rounded-[4px] px-2.5 py-1 font-mono text-[10px] font-medium transition-all ${
filterMode === mode
? "bg-surface text-ink border border-hairline-focus shadow-xs"
: "text-ink-muted hover:text-ink"
}`}
>
{mode.toUpperCase()}
</button>
))}
</div>
</div>
{/* Primary Metric Grid */}
<div className="mod-tile grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<MetricTile
label="Total Audited"
value={formatNumber(stats.total)}
hint="Continuous LLM inspection"
icon={<Shield className="size-4" />}
/>
<MetricTile
label="Clean Executed"
value={formatNumber(stats.executed)}
hint={`${Math.round((executed / total) * 100)}% execution rate`}
tone="signal"
icon={<CheckCircle2 className="size-4" />}
/>
<MetricTile
label="Pending / Failed"
value={formatNumber(stats.pending + stats.failed)}
hint={`${stats.failed} failed items`}
tone={failedRatio > 0.05 ? "vermilion" : "neutral"}
icon={<AlertTriangle className="size-4" />}
/>
{coverage && (
<MetricTile
label="Analysis Coverage"
value={`${Math.round(coverage.coverage_rate)}%`}
hint={`${formatNumber(coverage.completed)}/${formatNumber(coverage.total)} analyzed`}
tone={coverage.coverage_rate >= 90 ? "signal" : "amber"}
icon={<BarChart3 className="size-4" />}
/>
)}
</div>
{/* Live Moderation Stream */}
<div className="mod-tile">
<GlassPanel>
<SectionHeader
eyebrow="Realtime Feed"
title="Live Stream Audit Log"
/>
<div className="mt-3">
<LiveModerationFeed
actions={
actions?.filter((r) => {
if (filterMode === "flagged") return r.status === "failed";
if (filterMode === "clean") return r.status === "executed";
return true;
}) ?? []
}
/>
</div>
</GlassPanel>
</div>
{/* Heatmap Section */}
{hourly && hourly.length > 0 && (
<div className="mod-tile">
<GlassPanel>
<SectionHeader
eyebrow="Distribution"
title="Hourly Incident Heatmap"
/>
<div className="mt-3">
<ModerationHeatmap hours={hourly} />
</div>
</GlassPanel>
</div>
)}
{/* Domains, Channels, and Trends — 3-column row */}
<div className="grid gap-3 lg:grid-cols-3">
{/* Top Flagged Domains */}
{domains && domains.length > 0 && (
<div className="mod-tile">
<ScamDomains domains={domains} />
</div>
)}
{/* Top Flagged Channels */}
{channels && channels.length > 0 && (
<div className="mod-tile">
<TopChannels channels={channels} />
</div>
)}
{/* Moderation Trends */}
{trends && (
<div className="mod-tile">
<GlassPanel>
<SectionHeader eyebrow="trends" title="Category Breakdown" />
<div className="mt-3 space-y-3">
{/* Categories */}
{trends.categories.length > 0 && (
<div>
<div className="eyebrow mb-1.5">By Category</div>
<div className="flex flex-wrap gap-1.5">
{trends.categories.slice(0, 8).map((cat) => (
<span
key={cat.name}
className="inline-flex items-center gap-1 rounded-md border border-hairline bg-surface-2 px-2 py-1 text-[10px] font-medium text-ink-soft"
>
{cat.name}
<span className="font-mono text-ink-muted">
{cat.count}
</span>
</span>
))}
</div>
</div>
)}
{/* Severities */}
{trends.severities.length > 0 && (
<div>
<div className="eyebrow mb-1.5">By Severity</div>
<div className="space-y-1.5">
{trends.severities.map((sev) => {
const maxSev = Math.max(
...trends.severities.map((s) => s.count),
);
const pct = maxSev
? Math.round((sev.count / maxSev) * 100)
: 0;
const tone =
sev.level === "critical" || sev.level === "high"
? "bg-vermilion"
: sev.level === "medium"
? "bg-amber"
: "bg-signal";
return (
<div
key={sev.level}
className="flex items-center gap-2 text-[10px]"
>
<span className="w-16 shrink-0 font-mono text-ink-muted">
{sev.level}
</span>
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full rounded-full ${tone}`}
style={{ width: `${pct}%` }}
/>
</div>
<span className="w-8 text-right font-mono text-ink-faint">
{sev.count}
</span>
</div>
);
})}
</div>
</div>
)}
{/* Action Types */}
{trends.actions.length > 0 && (
<div>
<div className="eyebrow mb-1.5">By Action Type</div>
<div className="flex flex-wrap gap-1.5">
{trends.actions.map((act) => (
<span
key={act.type}
className="inline-flex items-center gap-1 rounded-md border border-vermilion/20 bg-vermilion/5 px-2 py-1 text-[10px] font-medium text-vermilion/80"
>
{act.type.replace(/_/g, " ")}
<span className="font-mono text-ink-muted">
{act.count}
</span>
</span>
))}
</div>
</div>
)}
</div>
</GlassPanel>
</div>
)}
</div>
{/* Coverage Progress Bar */}
{coverage && (
<div className="mod-tile">
<GlassPanel>
<SectionHeader
eyebrow="pipeline"
title="Analysis Coverage"
action={
<span className="font-mono text-[11px] text-ink-muted">
{formatNumber(coverage.completed)}/
{formatNumber(coverage.total)} (
{Math.round(coverage.coverage_rate)}%)
</span>
}
/>
<div className="mt-3">
<div className="h-2.5 w-full overflow-hidden rounded-full bg-surface-2">
<div
className={`h-full rounded-full transition-all ${
coverage.coverage_rate >= 90
? "bg-signal"
: coverage.coverage_rate >= 70
? "bg-amber"
: "bg-vermilion"
}`}
style={{
width: `${Math.min(100, coverage.coverage_rate)}%`,
}}
/>
</div>
<div className="mt-2 flex justify-between font-mono text-[10px] text-ink-muted">
<span>{formatNumber(coverage.completed)} analyzed</span>
<span>
{formatNumber(coverage.pending)} pending ·{" "}
{formatNumber(coverage.failed)} failed
</span>
</div>
</div>
</GlassPanel>
</div>
)}
</div>
</PageTransition>
);
}