feat: add moderation review dashboard

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-05-14 21:01:57 +07:00
parent fbf0f11db1
commit ae02556f75
5 changed files with 333 additions and 44 deletions

View File

@@ -0,0 +1,73 @@
import type { MessageRecord } from "../../api/client";
export interface MessageCardProps {
message: MessageRecord;
onReanalyze: (id: string) => void;
}
const STATUS_COLORS: Record<string, string> = {
pending: "#f9e2af",
clean: "#a6e3a1",
warn: "#fab387",
flagged: "#f38ba8",
error: "#f38ba8",
};
export function MessageCard({ message, onReanalyze }: MessageCardProps) {
const displayContent = message.edited_content ?? message.content;
const aiStatus = message.ai_status ?? "pending";
const statusColor = STATUS_COLORS[aiStatus] ?? "#6c7086";
return (
<div className={`message-card type-${message.type}`}>
<img
src={message.avatar_url ?? "/default-avatar.png"}
alt={message.username}
className="message-card-avatar"
width={32}
height={32}
/>
<div className="message-card-body">
<div className="message-card-meta">
<span className="message-card-username">{message.username}</span>
<span className="message-card-time">
{new Date(message.created_at).toLocaleString()}
</span>
{message.type === "edited" && (
<span className="badge badge-edited">edited</span>
)}
{message.type === "deleted" && (
<span className="badge badge-deleted">deleted</span>
)}
<span
className="badge badge-ai"
style={{ backgroundColor: statusColor }}
title={`AI: ${aiStatus}`}
>
{aiStatus}
</span>
</div>
<p className="message-card-content">{displayContent}</p>
{message.ai_analysis && (
<div className="message-card-analysis">{message.ai_analysis}</div>
)}
{message.ai_error && (
<div className="message-card-error">{message.ai_error}</div>
)}
<div className="message-card-actions">
<button
className="btn-reanalyze"
onClick={() => onReanalyze(message.id)}
disabled={aiStatus === "pending"}
>
Reanalyze
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import type { MessageRecord } from "../../api/client";
import { MessageCard } from "./MessageCard";
export interface MessageFeedProps {
messages: MessageRecord[];
onReanalyze: (id: string) => void;
}
export function MessageFeed({ messages, onReanalyze }: MessageFeedProps) {
if (messages.length === 0) {
return (
<div className="empty-state">
<p>No messages yet</p>
</div>
);
}
return (
<div className="message-feed">
{messages.map((msg) => (
<MessageCard key={msg.id} message={msg} onReanalyze={onReanalyze} />
))}
</div>
);
}

View File

@@ -0,0 +1,37 @@
import type { MessageRecord } from "../../api/client";
import { MessageCard } from "../messages/MessageCard";
export interface ReviewPanelProps {
messages: MessageRecord[];
onReanalyze: (id: string) => void;
}
export function ReviewPanel({ messages, onReanalyze }: ReviewPanelProps) {
const reviewItems = messages.filter(
(m) =>
m.ai_status === "warn" ||
m.ai_status === "flagged" ||
m.ai_status === "error",
);
return (
<div className="review-panel">
<div className="review-header">
<h2>Needs Review</h2>
<span className="review-count">{reviewItems.length}</span>
</div>
{reviewItems.length === 0 ? (
<div className="empty-state">
<p>No items to review</p>
</div>
) : (
<div className="review-list">
{reviewItems.map((msg) => (
<MessageCard key={msg.id} message={msg} onReanalyze={onReanalyze} />
))}
</div>
)}
</div>
);
}