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
This commit is contained in:
asepharyana committed 2026-09-01 23:51:39 +07:00
1 parent 7bfdf9c85c
commit 38b74cc1db
4 files changed
+784 -85

No files matched your search

@@ -1,11 +1,20 @@
"use client";
import { AlertTriangle, CheckCircle2, Shield } from "lucide-react";
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,
@@ -14,10 +23,15 @@ import {
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";
@@ -48,6 +62,10 @@ export function ModerationView({
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">(
@@ -99,6 +117,26 @@ export function ModerationView({
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
@@ -118,7 +156,7 @@ export function ModerationView({
</div>
{/* Primary Metric Grid */}
<div className="mod-tile grid gap-3 sm:grid-cols-3">
<div className="mod-tile grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<MetricTile
label="Total Audited"
value={formatNumber(stats.total)}
@@ -139,6 +177,15 @@ export function ModerationView({
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 */}
@@ -176,6 +223,157 @@ export function ModerationView({
</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>
);