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:
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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user