@burdenoff/microfe-comradecircle 2026.830.1 → 2026.1025.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/comradecircle/components/MetricsCard.js +64 -0
- package/dist/comradecircle/components/MetricsCard.js.map +1 -0
- package/dist/comradecircle/components/StatusBadge.d.ts.map +1 -1
- package/dist/comradecircle/components/StatusBadge.js +4 -0
- package/dist/comradecircle/components/StatusBadge.js.map +1 -1
- package/dist/comradecircle/hooks/index.d.ts +1 -0
- package/dist/comradecircle/hooks/index.d.ts.map +1 -1
- package/dist/comradecircle/hooks/index.js +1 -0
- package/dist/comradecircle/hooks/useComradeCircleApi.d.ts +6 -0
- package/dist/comradecircle/hooks/useComradeCircleApi.d.ts.map +1 -1
- package/dist/comradecircle/hooks/useComradeCircleApi.js +1 -1
- package/dist/comradecircle/hooks/useComradeCircleApi.js.map +1 -1
- package/dist/comradecircle/hooks/useModerationApi.d.ts +19 -0
- package/dist/comradecircle/hooks/useModerationApi.d.ts.map +1 -0
- package/dist/comradecircle/hooks/useModerationApi.js +101 -0
- package/dist/comradecircle/hooks/useModerationApi.js.map +1 -0
- package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts +1 -0
- package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts.map +1 -1
- package/dist/comradecircle/pages/circles/CircleDetailPage.js +91 -77
- package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/ModerationTab.d.ts +7 -0
- package/dist/comradecircle/pages/circles/ModerationTab.d.ts.map +1 -0
- package/dist/comradecircle/pages/circles/ModerationTab.js +429 -0
- package/dist/comradecircle/pages/circles/ModerationTab.js.map +1 -0
- package/dist/comradecircle/pages/events/EventsPage.js +6 -6
- package/dist/comradecircle/types/index.d.ts +53 -0
- package/dist/comradecircle/types/index.d.ts.map +1 -1
- package/dist/generated/global-types.d.ts +9105 -562
- package/dist/generated/global-types.d.ts.map +1 -1
- package/dist/generated/wspace-types.d.ts +103018 -27116
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { useComPost as e } from "../../hooks/useComradeCircleApi.js";
|
|
2
|
+
import { useTr as t } from "../../hooks/useTr.js";
|
|
3
|
+
import { useComCircleActivitySummary as n, useComCircleVerdicts as r, useComModerationRule as i, useComReviewVerdict as a, useComUpdateModerationRule as o } from "../../hooks/useModerationApi.js";
|
|
4
|
+
import "../../hooks/index.js";
|
|
5
|
+
import { describeError as s, notify as c } from "../../lib/toast.js";
|
|
6
|
+
import { formatDateTime as l, formatEnumLabel as u, formatNumber as d } from "../../utils/formatters.js";
|
|
7
|
+
import { EmptyState as f } from "../../components/EmptyState.js";
|
|
8
|
+
import { ErrorView as p, LoadingView as m } from "../../components/StateViews.js";
|
|
9
|
+
import { StatusBadge as h } from "../../components/StatusBadge.js";
|
|
10
|
+
import { FilterTabs as g } from "../../components/FilterTabs.js";
|
|
11
|
+
import { MetricsCard as _ } from "../../components/MetricsCard.js";
|
|
12
|
+
import { TagInput as v } from "../../components/TagInput.js";
|
|
13
|
+
import { useState as y } from "react";
|
|
14
|
+
import { Activity as b, AlertTriangle as x, CheckCircle2 as S, EyeOff as C, FileText as w, MessageCircle as T, RotateCcw as E, ShieldAlert as D, ShieldCheck as O, Sparkles as k, UserPlus as A, Users as j } from "lucide-react";
|
|
15
|
+
import { Fragment as M, jsx as N, jsxs as P } from "react/jsx-runtime";
|
|
16
|
+
import { Button as F, Label as I, Switch as L, Textarea as R } from "@burdenoff/fe-libs/ui";
|
|
17
|
+
//#region src/comradecircle/pages/circles/ModerationTab.tsx
|
|
18
|
+
var z = 100, B = 25;
|
|
19
|
+
function V({ circle: e, canEditRules: t }) {
|
|
20
|
+
return /* @__PURE__ */ P("div", {
|
|
21
|
+
className: "flex flex-col gap-6",
|
|
22
|
+
"data-testid": "moderation-tab",
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ N(H, { circleId: e.id }),
|
|
25
|
+
/* @__PURE__ */ N(U, { circleId: e.id }),
|
|
26
|
+
/* @__PURE__ */ N(q, {
|
|
27
|
+
circleId: e.id,
|
|
28
|
+
canEdit: t
|
|
29
|
+
})
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function H({ circleId: e }) {
|
|
34
|
+
let r = t(), [i, a] = y("7"), { data: o, isLoading: s, isError: c, refetch: l } = n(e, Number(i)), u = [{
|
|
35
|
+
value: "7",
|
|
36
|
+
label: r("comradecircle.moderation.summary.last7", "Last 7 days")
|
|
37
|
+
}, {
|
|
38
|
+
value: "30",
|
|
39
|
+
label: r("comradecircle.moderation.summary.last30", "Last 30 days")
|
|
40
|
+
}], f = (e) => o?.verdictCounts.find((t) => t.verdict === e)?.count ?? 0;
|
|
41
|
+
return /* @__PURE__ */ P("section", {
|
|
42
|
+
className: "rounded-2xl border border-border-seam bg-bg-surface p-5",
|
|
43
|
+
"aria-labelledby": "moderation-summary-heading",
|
|
44
|
+
children: [/* @__PURE__ */ P("div", {
|
|
45
|
+
className: "mb-4 flex flex-wrap items-center justify-between gap-3",
|
|
46
|
+
children: [/* @__PURE__ */ P("h2", {
|
|
47
|
+
id: "moderation-summary-heading",
|
|
48
|
+
className: "flex items-center gap-2 text-lg font-semibold text-text-primary",
|
|
49
|
+
children: [/* @__PURE__ */ N(b, { className: "h-5 w-5" }), r("comradecircle.moderation.summary.title", "Circle activity")]
|
|
50
|
+
}), /* @__PURE__ */ N(g, {
|
|
51
|
+
options: u,
|
|
52
|
+
value: i,
|
|
53
|
+
onChange: a
|
|
54
|
+
})]
|
|
55
|
+
}), s ? /* @__PURE__ */ N(m, { label: r("comradecircle.moderation.summary.loading", "Loading activity…") }) : c || !o ? /* @__PURE__ */ N(p, {
|
|
56
|
+
title: r("comradecircle.moderation.summary.errorTitle", "Could not load activity"),
|
|
57
|
+
onRetry: () => l()
|
|
58
|
+
}) : /* @__PURE__ */ P(M, { children: [
|
|
59
|
+
/* @__PURE__ */ N("p", {
|
|
60
|
+
className: "mb-4 text-sm text-text-secondary",
|
|
61
|
+
"data-testid": "moderation-summary-text",
|
|
62
|
+
children: o.summaryText
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ P("div", {
|
|
65
|
+
className: "grid grid-cols-2 gap-3 md:grid-cols-4",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ N(_, {
|
|
68
|
+
icon: /* @__PURE__ */ N(w, { className: "h-5 w-5" }),
|
|
69
|
+
value: d(o.totalPosts),
|
|
70
|
+
label: r("comradecircle.moderation.summary.posts", "Posts")
|
|
71
|
+
}),
|
|
72
|
+
/* @__PURE__ */ N(_, {
|
|
73
|
+
icon: /* @__PURE__ */ N(T, { className: "h-5 w-5" }),
|
|
74
|
+
value: d(o.totalComments),
|
|
75
|
+
label: r("comradecircle.moderation.summary.comments", "Comments")
|
|
76
|
+
}),
|
|
77
|
+
/* @__PURE__ */ N(_, {
|
|
78
|
+
icon: /* @__PURE__ */ N(A, { className: "h-5 w-5" }),
|
|
79
|
+
value: d(o.newMembers),
|
|
80
|
+
label: r("comradecircle.moderation.summary.newMembers", "New members")
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ N(_, {
|
|
83
|
+
icon: /* @__PURE__ */ N(j, { className: "h-5 w-5" }),
|
|
84
|
+
value: d(o.activeMembers),
|
|
85
|
+
label: r("comradecircle.moderation.summary.activeMembers", "Active members")
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ P("div", {
|
|
90
|
+
className: "mt-3 flex flex-wrap gap-2 text-xs text-text-secondary",
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ N("span", {
|
|
93
|
+
className: "rounded-full bg-bg-sunken px-2.5 py-1",
|
|
94
|
+
children: r("comradecircle.moderation.summary.flagged", "{{count}} flagged", { count: f("FLAG") })
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ N("span", {
|
|
97
|
+
className: "rounded-full bg-bg-sunken px-2.5 py-1",
|
|
98
|
+
children: r("comradecircle.moderation.summary.hidden", "{{count}} auto-hidden", { count: f("HIDE") })
|
|
99
|
+
}),
|
|
100
|
+
/* @__PURE__ */ N("span", {
|
|
101
|
+
className: "rounded-full bg-bg-sunken px-2.5 py-1",
|
|
102
|
+
children: r("comradecircle.moderation.summary.escalated", "{{count}} escalated", { count: f("ESCALATE") })
|
|
103
|
+
})
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
] })]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function U({ circleId: e }) {
|
|
110
|
+
let n = t(), [i, a] = y("pending"), { data: o, isLoading: s, isError: c, refetch: l } = r(e, { reviewed: i === "pending" ? !1 : i === "reviewed" ? !0 : void 0 }), u = (o ?? []).filter((e) => i !== "pending" || e.verdict !== "ALLOW"), d = [
|
|
111
|
+
{
|
|
112
|
+
value: "pending",
|
|
113
|
+
label: n("comradecircle.moderation.queue.pending", "Needs review")
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: "reviewed",
|
|
117
|
+
label: n("comradecircle.moderation.queue.reviewed", "Reviewed")
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
value: "all",
|
|
121
|
+
label: n("comradecircle.moderation.queue.all", "All decisions")
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
return /* @__PURE__ */ P("section", {
|
|
125
|
+
className: "rounded-2xl border border-border-seam bg-bg-surface p-5",
|
|
126
|
+
"aria-labelledby": "moderation-queue-heading",
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ P("div", {
|
|
129
|
+
className: "mb-2 flex flex-wrap items-center justify-between gap-3",
|
|
130
|
+
children: [/* @__PURE__ */ P("h2", {
|
|
131
|
+
id: "moderation-queue-heading",
|
|
132
|
+
className: "flex items-center gap-2 text-lg font-semibold text-text-primary",
|
|
133
|
+
children: [/* @__PURE__ */ N(D, { className: "h-5 w-5" }), n("comradecircle.moderation.queue.title", "Review queue")]
|
|
134
|
+
}), /* @__PURE__ */ N(g, {
|
|
135
|
+
options: d,
|
|
136
|
+
value: i,
|
|
137
|
+
onChange: a
|
|
138
|
+
})]
|
|
139
|
+
}),
|
|
140
|
+
/* @__PURE__ */ N("p", {
|
|
141
|
+
className: "mb-4 text-sm text-text-tertiary",
|
|
142
|
+
children: n("comradecircle.moderation.queue.explainer", "The moderation agent triages every new post and comment and suggests an action. It never removes content or bans members: anything beyond hiding obvious spam waits for you.")
|
|
143
|
+
}),
|
|
144
|
+
s ? /* @__PURE__ */ N(m, { label: n("comradecircle.moderation.queue.loading", "Loading queue…") }) : c ? /* @__PURE__ */ N(p, {
|
|
145
|
+
title: n("comradecircle.moderation.queue.errorTitle", "Could not load the queue"),
|
|
146
|
+
onRetry: () => l()
|
|
147
|
+
}) : u.length === 0 ? /* @__PURE__ */ N(f, {
|
|
148
|
+
icon: /* @__PURE__ */ N(O, { className: "h-6 w-6" }),
|
|
149
|
+
title: i === "pending" ? n("comradecircle.moderation.queue.emptyPending", "Nothing needs review") : n("comradecircle.moderation.queue.empty", "No moderation decisions yet"),
|
|
150
|
+
description: n("comradecircle.moderation.queue.emptyDescription", "Flagged, hidden and escalated content will appear here.")
|
|
151
|
+
}) : /* @__PURE__ */ N("ul", {
|
|
152
|
+
className: "flex flex-col gap-3",
|
|
153
|
+
"data-testid": "moderation-queue",
|
|
154
|
+
children: u.map((t) => /* @__PURE__ */ N(G, {
|
|
155
|
+
verdict: t,
|
|
156
|
+
circleId: e
|
|
157
|
+
}, t.id))
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function W(e, t) {
|
|
163
|
+
switch (e.verdict) {
|
|
164
|
+
case "HIDE": return e.actedAutomatically ? {
|
|
165
|
+
text: t("comradecircle.moderation.suggest.hideActed", "Hidden automatically as high-confidence spam. Uphold to keep it hidden, or overturn to restore it."),
|
|
166
|
+
tone: "warning"
|
|
167
|
+
} : {
|
|
168
|
+
text: t("comradecircle.moderation.suggest.hide", "Suggests hiding this content. No action was taken."),
|
|
169
|
+
tone: "warning"
|
|
170
|
+
};
|
|
171
|
+
case "ESCALATE": return {
|
|
172
|
+
text: t("comradecircle.moderation.suggest.escalate", "Needs a human decision. Possible harassment is never acted on automatically."),
|
|
173
|
+
tone: "error"
|
|
174
|
+
};
|
|
175
|
+
case "FLAG": return {
|
|
176
|
+
text: t("comradecircle.moderation.suggest.flag", "Suggests a review. Nothing was hidden."),
|
|
177
|
+
tone: "warning"
|
|
178
|
+
};
|
|
179
|
+
default: return {
|
|
180
|
+
text: t("comradecircle.moderation.suggest.allow", "No action suggested."),
|
|
181
|
+
tone: "neutral"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function G({ verdict: e, circleId: n }) {
|
|
186
|
+
let r = t(), i = a(n), [o, d] = y(""), f = W(e, r), p = !!e.reviewedAt, m = (t) => i.mutateAsync({
|
|
187
|
+
verdictId: e.id,
|
|
188
|
+
decision: t,
|
|
189
|
+
note: o.trim()
|
|
190
|
+
}).then(() => {
|
|
191
|
+
d(""), c.success(t === "OVERTURN" ? r("comradecircle.moderation.review.overturned", "Decision overturned") : r("comradecircle.moderation.review.upheld", "Decision upheld"));
|
|
192
|
+
}).catch((e) => c.error(r("comradecircle.moderation.review.error", "Could not record the review"), s(e)));
|
|
193
|
+
return /* @__PURE__ */ P("li", {
|
|
194
|
+
className: "rounded-xl border border-border-seam bg-bg-base p-4",
|
|
195
|
+
"data-testid": "moderation-verdict",
|
|
196
|
+
"data-verdict-id": e.id,
|
|
197
|
+
children: [
|
|
198
|
+
/* @__PURE__ */ P("div", {
|
|
199
|
+
className: "flex flex-wrap items-center gap-2",
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ N(h, { status: e.verdict }),
|
|
202
|
+
/* @__PURE__ */ N("span", {
|
|
203
|
+
className: "text-xs font-medium text-text-secondary",
|
|
204
|
+
children: u(e.targetType)
|
|
205
|
+
}),
|
|
206
|
+
e.categories.map((e) => /* @__PURE__ */ N("span", {
|
|
207
|
+
className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
|
|
208
|
+
children: u(e)
|
|
209
|
+
}, e)),
|
|
210
|
+
/* @__PURE__ */ N("span", {
|
|
211
|
+
className: "text-xs text-text-tertiary",
|
|
212
|
+
children: r("comradecircle.moderation.verdict.confidence", "{{value}}% confidence", { value: Math.round(e.confidence * 100) })
|
|
213
|
+
}),
|
|
214
|
+
e.actedAutomatically && /* @__PURE__ */ P("span", {
|
|
215
|
+
className: "inline-flex items-center gap-1 text-xs text-status-warning-text",
|
|
216
|
+
children: [/* @__PURE__ */ N(C, { className: "h-3.5 w-3.5" }), r("comradecircle.moderation.verdict.autoHidden", "Auto-hidden")]
|
|
217
|
+
}),
|
|
218
|
+
e.injectionDetected && /* @__PURE__ */ P("span", {
|
|
219
|
+
className: "inline-flex items-center gap-1 text-xs text-status-error-text",
|
|
220
|
+
children: [/* @__PURE__ */ N(x, { className: "h-3.5 w-3.5" }), r("comradecircle.moderation.verdict.injection", "Prompt-injection attempt")]
|
|
221
|
+
}),
|
|
222
|
+
/* @__PURE__ */ N("span", {
|
|
223
|
+
className: "ml-auto text-xs text-text-tertiary",
|
|
224
|
+
children: l(e.createdAt)
|
|
225
|
+
})
|
|
226
|
+
]
|
|
227
|
+
}),
|
|
228
|
+
e.targetType === "POST" ? /* @__PURE__ */ N(K, { postId: e.targetId }) : /* @__PURE__ */ N("p", {
|
|
229
|
+
className: "mt-3 text-sm text-text-tertiary",
|
|
230
|
+
children: r("comradecircle.moderation.verdict.targetRef", "{{type}} {{id}}", {
|
|
231
|
+
type: u(e.targetType),
|
|
232
|
+
id: e.targetId
|
|
233
|
+
})
|
|
234
|
+
}),
|
|
235
|
+
/* @__PURE__ */ P("div", {
|
|
236
|
+
className: "mt-3 flex items-start gap-2 rounded-lg bg-bg-sunken p-3 text-sm",
|
|
237
|
+
children: [/* @__PURE__ */ N(k, { className: "mt-0.5 h-4 w-4 shrink-0 text-text-secondary" }), /* @__PURE__ */ P("div", { children: [/* @__PURE__ */ N("p", {
|
|
238
|
+
className: f.tone === "error" ? "font-medium text-status-error-text" : f.tone === "warning" ? "font-medium text-status-warning-text" : "font-medium text-text-primary",
|
|
239
|
+
children: f.text
|
|
240
|
+
}), /* @__PURE__ */ N("p", {
|
|
241
|
+
className: "mt-1 text-text-secondary",
|
|
242
|
+
children: e.rationale
|
|
243
|
+
})] })]
|
|
244
|
+
}),
|
|
245
|
+
p ? /* @__PURE__ */ P("p", {
|
|
246
|
+
className: "mt-3 flex items-center gap-1.5 text-sm text-text-secondary",
|
|
247
|
+
children: [
|
|
248
|
+
e.overturned ? /* @__PURE__ */ N(E, { className: "h-4 w-4" }) : /* @__PURE__ */ N(S, { className: "h-4 w-4" }),
|
|
249
|
+
e.overturned ? r("comradecircle.moderation.verdict.wasOverturned", "Overturned {{when}}", { when: l(e.reviewedAt) }) : r("comradecircle.moderation.verdict.wasUpheld", "Upheld {{when}}", { when: l(e.reviewedAt) }),
|
|
250
|
+
e.reviewNote ? ` · ${e.reviewNote}` : ""
|
|
251
|
+
]
|
|
252
|
+
}) : /* @__PURE__ */ P("div", {
|
|
253
|
+
className: "mt-3 flex flex-col gap-2",
|
|
254
|
+
children: [
|
|
255
|
+
/* @__PURE__ */ N(I, {
|
|
256
|
+
htmlFor: `review-note-${e.id}`,
|
|
257
|
+
className: "sr-only",
|
|
258
|
+
children: r("comradecircle.moderation.review.noteLabel", "Review note")
|
|
259
|
+
}),
|
|
260
|
+
/* @__PURE__ */ N(R, {
|
|
261
|
+
id: `review-note-${e.id}`,
|
|
262
|
+
value: o,
|
|
263
|
+
onChange: (e) => d(e.target.value),
|
|
264
|
+
placeholder: r("comradecircle.moderation.review.notePlaceholder", "Optional note for the audit trail"),
|
|
265
|
+
rows: 2,
|
|
266
|
+
maxLength: 500
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ P("div", {
|
|
269
|
+
className: "flex flex-wrap justify-end gap-2",
|
|
270
|
+
children: [/* @__PURE__ */ P(F, {
|
|
271
|
+
variant: "outline",
|
|
272
|
+
size: "sm",
|
|
273
|
+
onClick: () => m("OVERTURN"),
|
|
274
|
+
disabled: i.isPending,
|
|
275
|
+
"data-testid": "moderation-overturn",
|
|
276
|
+
children: [/* @__PURE__ */ N(E, { className: "mr-1.5 h-4 w-4" }), e.actedAutomatically ? r("comradecircle.moderation.review.overturnRestore", "Overturn and restore") : r("comradecircle.moderation.review.overturn", "Overturn")]
|
|
277
|
+
}), /* @__PURE__ */ P(F, {
|
|
278
|
+
size: "sm",
|
|
279
|
+
onClick: () => m("UPHOLD"),
|
|
280
|
+
disabled: i.isPending,
|
|
281
|
+
"data-testid": "moderation-uphold",
|
|
282
|
+
children: [/* @__PURE__ */ N(S, { className: "mr-1.5 h-4 w-4" }), r("comradecircle.moderation.review.uphold", "Uphold")]
|
|
283
|
+
})]
|
|
284
|
+
})
|
|
285
|
+
]
|
|
286
|
+
})
|
|
287
|
+
]
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function K({ postId: n }) {
|
|
291
|
+
let r = t(), { data: i, isLoading: a } = e(n);
|
|
292
|
+
return a ? /* @__PURE__ */ N("p", {
|
|
293
|
+
className: "mt-3 text-sm text-text-tertiary",
|
|
294
|
+
children: r("comradecircle.moderation.verdict.loadingPost", "Loading post…")
|
|
295
|
+
}) : i ? /* @__PURE__ */ P("div", {
|
|
296
|
+
className: "mt-3",
|
|
297
|
+
children: [/* @__PURE__ */ N("div", {
|
|
298
|
+
className: "mb-1 flex items-center gap-2",
|
|
299
|
+
children: /* @__PURE__ */ N(h, { status: i.status })
|
|
300
|
+
}), /* @__PURE__ */ N("p", {
|
|
301
|
+
className: "line-clamp-4 whitespace-pre-wrap break-words text-sm text-text-primary",
|
|
302
|
+
children: i.body
|
|
303
|
+
})]
|
|
304
|
+
}) : /* @__PURE__ */ N("p", {
|
|
305
|
+
className: "mt-3 text-sm text-text-tertiary",
|
|
306
|
+
children: r("comradecircle.moderation.verdict.postGone", "This post is no longer available.")
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function q({ circleId: e, canEdit: n }) {
|
|
310
|
+
let r = t(), { data: a, isLoading: o, isError: s, refetch: c } = i(e);
|
|
311
|
+
return /* @__PURE__ */ P("section", {
|
|
312
|
+
className: "rounded-2xl border border-border-seam bg-bg-surface p-5",
|
|
313
|
+
"aria-labelledby": "moderation-rule-heading",
|
|
314
|
+
children: [
|
|
315
|
+
/* @__PURE__ */ P("h2", {
|
|
316
|
+
id: "moderation-rule-heading",
|
|
317
|
+
className: "mb-1 flex items-center gap-2 text-lg font-semibold text-text-primary",
|
|
318
|
+
children: [/* @__PURE__ */ N(O, { className: "h-5 w-5" }), r("comradecircle.moderation.rule.title", "Auto-moderation rules")]
|
|
319
|
+
}),
|
|
320
|
+
/* @__PURE__ */ N("p", {
|
|
321
|
+
className: "mb-4 text-sm text-text-tertiary",
|
|
322
|
+
children: n ? r("comradecircle.moderation.rule.explainer", "Content matching a flagged term or spam pattern is queued for review. Auto-hide applies to spam only and can always be overturned.") : r("comradecircle.moderation.rule.readOnly", "Only circle owners and admins can change these rules.")
|
|
323
|
+
}),
|
|
324
|
+
o ? /* @__PURE__ */ N(m, { label: r("comradecircle.moderation.rule.loading", "Loading rules…") }) : s || !a ? /* @__PURE__ */ N(p, {
|
|
325
|
+
title: r("comradecircle.moderation.rule.errorTitle", "Could not load the rules"),
|
|
326
|
+
onRetry: () => c()
|
|
327
|
+
}) : /* @__PURE__ */ N(J, {
|
|
328
|
+
rule: a,
|
|
329
|
+
circleId: e,
|
|
330
|
+
canEdit: n
|
|
331
|
+
}, a.updatedAt)
|
|
332
|
+
]
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function J({ rule: e, circleId: n, canEdit: r }) {
|
|
336
|
+
let i = t(), a = o(n), [l, u] = y(e.enabled), [d, f] = y(e.autoHideSpam), [p, m] = y(e.flaggedTerms), [h, g] = y(e.spamPatterns.join("\n")), _ = h.split("\n").map((e) => e.trim()).filter(Boolean), b = _.length > B, x = () => a.mutateAsync({
|
|
337
|
+
enabled: l,
|
|
338
|
+
autoHideSpam: d,
|
|
339
|
+
flaggedTerms: p,
|
|
340
|
+
spamPatterns: _
|
|
341
|
+
}).then(() => c.success(i("comradecircle.moderation.rule.saved", "Moderation rules saved"))).catch((e) => c.error(i("comradecircle.moderation.rule.saveError", "Could not save the rules"), s(e)));
|
|
342
|
+
return /* @__PURE__ */ P("form", {
|
|
343
|
+
className: "flex flex-col gap-5",
|
|
344
|
+
"data-testid": "moderation-rule-form",
|
|
345
|
+
onSubmit: (e) => {
|
|
346
|
+
e.preventDefault(), x();
|
|
347
|
+
},
|
|
348
|
+
children: [
|
|
349
|
+
/* @__PURE__ */ P("div", {
|
|
350
|
+
className: "flex items-center justify-between gap-4",
|
|
351
|
+
children: [/* @__PURE__ */ P("div", { children: [/* @__PURE__ */ N(I, {
|
|
352
|
+
htmlFor: "rule-enabled",
|
|
353
|
+
children: i("comradecircle.moderation.rule.enabled", "Apply circle rules")
|
|
354
|
+
}), /* @__PURE__ */ N("p", {
|
|
355
|
+
className: "text-xs text-text-tertiary",
|
|
356
|
+
children: i("comradecircle.moderation.rule.enabledHint", "Built-in spam, harassment and prompt-injection checks always run.")
|
|
357
|
+
})] }), /* @__PURE__ */ N(L, {
|
|
358
|
+
id: "rule-enabled",
|
|
359
|
+
checked: l,
|
|
360
|
+
onCheckedChange: u,
|
|
361
|
+
disabled: !r
|
|
362
|
+
})]
|
|
363
|
+
}),
|
|
364
|
+
/* @__PURE__ */ P("div", {
|
|
365
|
+
className: "flex flex-col gap-1.5",
|
|
366
|
+
children: [/* @__PURE__ */ N(I, {
|
|
367
|
+
htmlFor: "rule-terms",
|
|
368
|
+
children: i("comradecircle.moderation.rule.terms", "Flagged terms")
|
|
369
|
+
}), /* @__PURE__ */ N(v, {
|
|
370
|
+
id: "rule-terms",
|
|
371
|
+
value: p,
|
|
372
|
+
onChange: r ? m : () => void 0,
|
|
373
|
+
max: z,
|
|
374
|
+
placeholder: i("comradecircle.moderation.rule.termsPlaceholder", "Type a word or phrase and press Enter")
|
|
375
|
+
})]
|
|
376
|
+
}),
|
|
377
|
+
/* @__PURE__ */ P("div", {
|
|
378
|
+
className: "flex flex-col gap-1.5",
|
|
379
|
+
children: [
|
|
380
|
+
/* @__PURE__ */ N(I, {
|
|
381
|
+
htmlFor: "rule-patterns",
|
|
382
|
+
children: i("comradecircle.moderation.rule.patterns", "Spam patterns (one per line)")
|
|
383
|
+
}),
|
|
384
|
+
/* @__PURE__ */ N(R, {
|
|
385
|
+
id: "rule-patterns",
|
|
386
|
+
value: h,
|
|
387
|
+
onChange: (e) => g(e.target.value),
|
|
388
|
+
rows: 4,
|
|
389
|
+
disabled: !r,
|
|
390
|
+
className: "font-mono text-sm",
|
|
391
|
+
placeholder: "promo-code-[0-9]{4}\nhttps?://bit\\.ly/\\w+"
|
|
392
|
+
}),
|
|
393
|
+
/* @__PURE__ */ N("p", {
|
|
394
|
+
className: b ? "text-xs text-status-error-text" : "text-xs text-text-tertiary",
|
|
395
|
+
children: i("comradecircle.moderation.rule.patternsHint", "Case-insensitive regular expressions, up to {{max}}. Nested quantifiers, backreferences and lookarounds are rejected to keep triage fast.", { max: B })
|
|
396
|
+
})
|
|
397
|
+
]
|
|
398
|
+
}),
|
|
399
|
+
/* @__PURE__ */ P("div", {
|
|
400
|
+
className: "flex items-center justify-between gap-4",
|
|
401
|
+
children: [/* @__PURE__ */ P("div", { children: [/* @__PURE__ */ N(I, {
|
|
402
|
+
htmlFor: "rule-autohide",
|
|
403
|
+
children: i("comradecircle.moderation.rule.autoHide", "Auto-hide matching spam posts")
|
|
404
|
+
}), /* @__PURE__ */ N("p", {
|
|
405
|
+
className: "text-xs text-text-tertiary",
|
|
406
|
+
children: i("comradecircle.moderation.rule.autoHideHint", "Hidden posts stay in the review queue and can be restored. Harassment and toxicity always go to a human.")
|
|
407
|
+
})] }), /* @__PURE__ */ N(L, {
|
|
408
|
+
id: "rule-autohide",
|
|
409
|
+
checked: d,
|
|
410
|
+
onCheckedChange: f,
|
|
411
|
+
disabled: !r
|
|
412
|
+
})]
|
|
413
|
+
}),
|
|
414
|
+
r && /* @__PURE__ */ N("div", {
|
|
415
|
+
className: "flex justify-end",
|
|
416
|
+
children: /* @__PURE__ */ N(F, {
|
|
417
|
+
type: "submit",
|
|
418
|
+
disabled: a.isPending || b,
|
|
419
|
+
"data-testid": "moderation-rule-save",
|
|
420
|
+
children: a.isPending ? i("comradecircle.moderation.rule.saving", "Saving…") : i("comradecircle.moderation.rule.save", "Save rules")
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
]
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
//#endregion
|
|
427
|
+
export { V as ModerationTab };
|
|
428
|
+
|
|
429
|
+
//# sourceMappingURL=ModerationTab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModerationTab.js","names":[],"sources":["../../../../src/comradecircle/pages/circles/ModerationTab.tsx"],"sourcesContent":["/**\n * ModerationTab — the moderator console for one circle (BOFF-4451).\n * @module comradecircle/pages/circles\n *\n * Three panels, each backed by a real `com*` field:\n * - Activity summary → `comCircleActivitySummary(circleId, days)`\n * - Review queue → `comListVerdicts(circleId, reviewed)` + `comReviewVerdict`\n * - Auto-moderation → `comModerationRule(circleId)` + `comUpdateModerationRule`\n *\n * The triage agent only ever suggests, except for one reversible action: it\n * may auto-hide high-confidence spam when a circle admin opts in. Every other\n * outcome waits here for a human decision. MODERATOR+ can review; only\n * ADMIN+ can change the rule (the svc enforces both).\n */\n\nimport { useState } from 'react';\nimport {\n Activity,\n AlertTriangle,\n CheckCircle2,\n EyeOff,\n FileText,\n MessageCircle,\n RotateCcw,\n ShieldAlert,\n ShieldCheck,\n Sparkles,\n UserPlus,\n Users,\n} from 'lucide-react';\nimport { Button, Label, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport { EmptyState } from '../../components/EmptyState';\nimport { FilterTabs, type FilterOption } from '../../components/FilterTabs';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { TagInput } from '../../components/TagInput';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport {\n useComCircleActivitySummary,\n useComCircleVerdicts,\n useComModerationRule,\n useComPost,\n useComReviewVerdict,\n useComUpdateModerationRule,\n useTr,\n} from '../../hooks';\nimport { formatDateTime, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { notify, describeError } from '../../lib/toast';\nimport type {\n ComCircle,\n ComModerationReviewDecision,\n ComModerationRule,\n ComModerationVerdict,\n} from '../../types';\n\n/** Mirrors the svc caps in moderation-rule-validation.ts. */\nconst MAX_FLAGGED_TERMS = 100;\nconst MAX_SPAM_PATTERNS = 25;\n\ntype QueueFilter = 'pending' | 'reviewed' | 'all';\ntype SummaryWindow = '7' | '30';\n\nexport function ModerationTab({\n circle,\n canEditRules,\n}: {\n circle: ComCircle;\n canEditRules: boolean;\n}) {\n return (\n <div className=\"flex flex-col gap-6\" data-testid=\"moderation-tab\">\n <ActivitySummaryPanel circleId={circle.id} />\n <ReviewQueuePanel circleId={circle.id} />\n <RulePanel circleId={circle.id} canEdit={canEditRules} />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Activity summary\n// ---------------------------------------------------------------------------\n\nfunction ActivitySummaryPanel({ circleId }: { circleId: string }) {\n const tr = useTr();\n const [window, setWindow] = useState<SummaryWindow>('7');\n const { data, isLoading, isError, refetch } = useComCircleActivitySummary(\n circleId,\n Number(window)\n );\n\n const windowOptions: FilterOption<SummaryWindow>[] = [\n { value: '7', label: tr('comradecircle.moderation.summary.last7', 'Last 7 days') },\n { value: '30', label: tr('comradecircle.moderation.summary.last30', 'Last 30 days') },\n ];\n\n const countOf = (verdict: string) =>\n data?.verdictCounts.find((v) => v.verdict === verdict)?.count ?? 0;\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-summary-heading\"\n >\n <div className=\"mb-4 flex flex-wrap items-center justify-between gap-3\">\n <h2\n id=\"moderation-summary-heading\"\n className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <Activity className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.summary.title', 'Circle activity')}\n </h2>\n <FilterTabs options={windowOptions} value={window} onChange={setWindow} />\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.summary.loading', 'Loading activity…')} />\n ) : isError || !data ? (\n <ErrorView\n title={tr('comradecircle.moderation.summary.errorTitle', 'Could not load activity')}\n onRetry={() => refetch()}\n />\n ) : (\n <>\n <p className=\"mb-4 text-sm text-text-secondary\" data-testid=\"moderation-summary-text\">\n {data.summaryText}\n </p>\n <div className=\"grid grid-cols-2 gap-3 md:grid-cols-4\">\n <MetricsCard\n icon={<FileText className=\"h-5 w-5\" />}\n value={formatNumber(data.totalPosts)}\n label={tr('comradecircle.moderation.summary.posts', 'Posts')}\n />\n <MetricsCard\n icon={<MessageCircle className=\"h-5 w-5\" />}\n value={formatNumber(data.totalComments)}\n label={tr('comradecircle.moderation.summary.comments', 'Comments')}\n />\n <MetricsCard\n icon={<UserPlus className=\"h-5 w-5\" />}\n value={formatNumber(data.newMembers)}\n label={tr('comradecircle.moderation.summary.newMembers', 'New members')}\n />\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={formatNumber(data.activeMembers)}\n label={tr('comradecircle.moderation.summary.activeMembers', 'Active members')}\n />\n </div>\n <div className=\"mt-3 flex flex-wrap gap-2 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.flagged', '{{count}} flagged', {\n count: countOf('FLAG'),\n })}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.hidden', '{{count}} auto-hidden', {\n count: countOf('HIDE'),\n })}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr('comradecircle.moderation.summary.escalated', '{{count}} escalated', {\n count: countOf('ESCALATE'),\n })}\n </span>\n </div>\n </>\n )}\n </section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Review queue\n// ---------------------------------------------------------------------------\n\nfunction ReviewQueuePanel({ circleId }: { circleId: string }) {\n const tr = useTr();\n const [filter, setFilter] = useState<QueueFilter>('pending');\n const reviewed = filter === 'pending' ? false : filter === 'reviewed' ? true : undefined;\n const { data, isLoading, isError, refetch } = useComCircleVerdicts(circleId, { reviewed });\n\n // ALLOW verdicts need nothing from a human; keep them out of the pending queue.\n const items = (data ?? []).filter((v) => filter !== 'pending' || v.verdict !== 'ALLOW');\n\n const filterOptions: FilterOption<QueueFilter>[] = [\n { value: 'pending', label: tr('comradecircle.moderation.queue.pending', 'Needs review') },\n { value: 'reviewed', label: tr('comradecircle.moderation.queue.reviewed', 'Reviewed') },\n { value: 'all', label: tr('comradecircle.moderation.queue.all', 'All decisions') },\n ];\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-queue-heading\"\n >\n <div className=\"mb-2 flex flex-wrap items-center justify-between gap-3\">\n <h2\n id=\"moderation-queue-heading\"\n className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <ShieldAlert className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.queue.title', 'Review queue')}\n </h2>\n <FilterTabs options={filterOptions} value={filter} onChange={setFilter} />\n </div>\n <p className=\"mb-4 text-sm text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.queue.explainer',\n 'The moderation agent triages every new post and comment and suggests an action. It never removes content or bans members: anything beyond hiding obvious spam waits for you.'\n )}\n </p>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.queue.loading', 'Loading queue…')} />\n ) : isError ? (\n <ErrorView\n title={tr('comradecircle.moderation.queue.errorTitle', 'Could not load the queue')}\n onRetry={() => refetch()}\n />\n ) : items.length === 0 ? (\n <EmptyState\n icon={<ShieldCheck className=\"h-6 w-6\" />}\n title={\n filter === 'pending'\n ? tr('comradecircle.moderation.queue.emptyPending', 'Nothing needs review')\n : tr('comradecircle.moderation.queue.empty', 'No moderation decisions yet')\n }\n description={tr(\n 'comradecircle.moderation.queue.emptyDescription',\n 'Flagged, hidden and escalated content will appear here.'\n )}\n />\n ) : (\n <ul className=\"flex flex-col gap-3\" data-testid=\"moderation-queue\">\n {items.map((verdict) => (\n <VerdictCard key={verdict.id} verdict={verdict} circleId={circleId} />\n ))}\n </ul>\n )}\n </section>\n );\n}\n\nfunction suggestedAction(\n verdict: ComModerationVerdict,\n tr: ReturnType<typeof useTr>\n): { text: string; tone: 'warning' | 'error' | 'neutral' } {\n switch (verdict.verdict) {\n case 'HIDE':\n return verdict.actedAutomatically\n ? {\n text: tr(\n 'comradecircle.moderation.suggest.hideActed',\n 'Hidden automatically as high-confidence spam. Uphold to keep it hidden, or overturn to restore it.'\n ),\n tone: 'warning',\n }\n : {\n text: tr(\n 'comradecircle.moderation.suggest.hide',\n 'Suggests hiding this content. No action was taken.'\n ),\n tone: 'warning',\n };\n case 'ESCALATE':\n return {\n text: tr(\n 'comradecircle.moderation.suggest.escalate',\n 'Needs a human decision. Possible harassment is never acted on automatically.'\n ),\n tone: 'error',\n };\n case 'FLAG':\n return {\n text: tr('comradecircle.moderation.suggest.flag', 'Suggests a review. Nothing was hidden.'),\n tone: 'warning',\n };\n default:\n return {\n text: tr('comradecircle.moderation.suggest.allow', 'No action suggested.'),\n tone: 'neutral',\n };\n }\n}\n\nfunction VerdictCard({ verdict, circleId }: { verdict: ComModerationVerdict; circleId: string }) {\n const tr = useTr();\n const review = useComReviewVerdict(circleId);\n const [note, setNote] = useState('');\n const suggestion = suggestedAction(verdict, tr);\n const isReviewed = Boolean(verdict.reviewedAt);\n\n const submit = (decision: ComModerationReviewDecision) =>\n review\n .mutateAsync({ verdictId: verdict.id, decision, note: note.trim() })\n .then(() => {\n setNote('');\n notify.success(\n decision === 'OVERTURN'\n ? tr('comradecircle.moderation.review.overturned', 'Decision overturned')\n : tr('comradecircle.moderation.review.upheld', 'Decision upheld')\n );\n })\n .catch((e) =>\n notify.error(\n tr('comradecircle.moderation.review.error', 'Could not record the review'),\n describeError(e)\n )\n );\n\n return (\n <li\n className=\"rounded-xl border border-border-seam bg-bg-base p-4\"\n data-testid=\"moderation-verdict\"\n data-verdict-id={verdict.id}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={verdict.verdict} />\n <span className=\"text-xs font-medium text-text-secondary\">\n {formatEnumLabel(verdict.targetType)}\n </span>\n {verdict.categories.map((category) => (\n <span\n key={category}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {formatEnumLabel(category)}\n </span>\n ))}\n <span className=\"text-xs text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.confidence', '{{value}}% confidence', {\n value: Math.round(verdict.confidence * 100),\n })}\n </span>\n {verdict.actedAutomatically && (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-warning-text\">\n <EyeOff className=\"h-3.5 w-3.5\" />\n {tr('comradecircle.moderation.verdict.autoHidden', 'Auto-hidden')}\n </span>\n )}\n {verdict.injectionDetected && (\n <span className=\"inline-flex items-center gap-1 text-xs text-status-error-text\">\n <AlertTriangle className=\"h-3.5 w-3.5\" />\n {tr('comradecircle.moderation.verdict.injection', 'Prompt-injection attempt')}\n </span>\n )}\n <span className=\"ml-auto text-xs text-text-tertiary\">\n {formatDateTime(verdict.createdAt)}\n </span>\n </div>\n\n {verdict.targetType === 'POST' ? (\n <PostPreview postId={verdict.targetId} />\n ) : (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.targetRef', '{{type}} {{id}}', {\n type: formatEnumLabel(verdict.targetType),\n id: verdict.targetId,\n })}\n </p>\n )}\n\n <div className=\"mt-3 flex items-start gap-2 rounded-lg bg-bg-sunken p-3 text-sm\">\n <Sparkles className=\"mt-0.5 h-4 w-4 shrink-0 text-text-secondary\" />\n <div>\n <p\n className={\n suggestion.tone === 'error'\n ? 'font-medium text-status-error-text'\n : suggestion.tone === 'warning'\n ? 'font-medium text-status-warning-text'\n : 'font-medium text-text-primary'\n }\n >\n {suggestion.text}\n </p>\n <p className=\"mt-1 text-text-secondary\">{verdict.rationale}</p>\n </div>\n </div>\n\n {isReviewed ? (\n <p className=\"mt-3 flex items-center gap-1.5 text-sm text-text-secondary\">\n {verdict.overturned ? (\n <RotateCcw className=\"h-4 w-4\" />\n ) : (\n <CheckCircle2 className=\"h-4 w-4\" />\n )}\n {verdict.overturned\n ? tr('comradecircle.moderation.verdict.wasOverturned', 'Overturned {{when}}', {\n when: formatDateTime(verdict.reviewedAt),\n })\n : tr('comradecircle.moderation.verdict.wasUpheld', 'Upheld {{when}}', {\n when: formatDateTime(verdict.reviewedAt),\n })}\n {verdict.reviewNote ? ` · ${verdict.reviewNote}` : ''}\n </p>\n ) : (\n <div className=\"mt-3 flex flex-col gap-2\">\n <Label htmlFor={`review-note-${verdict.id}`} className=\"sr-only\">\n {tr('comradecircle.moderation.review.noteLabel', 'Review note')}\n </Label>\n <Textarea\n id={`review-note-${verdict.id}`}\n value={note}\n onChange={(e) => setNote(e.target.value)}\n placeholder={tr(\n 'comradecircle.moderation.review.notePlaceholder',\n 'Optional note for the audit trail'\n )}\n rows={2}\n maxLength={500}\n />\n <div className=\"flex flex-wrap justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => submit('OVERTURN')}\n disabled={review.isPending}\n data-testid=\"moderation-overturn\"\n >\n <RotateCcw className=\"mr-1.5 h-4 w-4\" />\n {verdict.actedAutomatically\n ? tr('comradecircle.moderation.review.overturnRestore', 'Overturn and restore')\n : tr('comradecircle.moderation.review.overturn', 'Overturn')}\n </Button>\n <Button\n size=\"sm\"\n onClick={() => submit('UPHOLD')}\n disabled={review.isPending}\n data-testid=\"moderation-uphold\"\n >\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n {tr('comradecircle.moderation.review.uphold', 'Uphold')}\n </Button>\n </div>\n </div>\n )}\n </li>\n );\n}\n\nfunction PostPreview({ postId }: { postId: string }) {\n const tr = useTr();\n const { data: post, isLoading } = useComPost(postId);\n if (isLoading) {\n return (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.loadingPost', 'Loading post…')}\n </p>\n );\n }\n if (!post) {\n return (\n <p className=\"mt-3 text-sm text-text-tertiary\">\n {tr('comradecircle.moderation.verdict.postGone', 'This post is no longer available.')}\n </p>\n );\n }\n return (\n <div className=\"mt-3\">\n <div className=\"mb-1 flex items-center gap-2\">\n <StatusBadge status={post.status} />\n </div>\n <p className=\"line-clamp-4 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Auto-moderation rule\n// ---------------------------------------------------------------------------\n\nfunction RulePanel({ circleId, canEdit }: { circleId: string; canEdit: boolean }) {\n const tr = useTr();\n const { data: rule, isLoading, isError, refetch } = useComModerationRule(circleId);\n\n return (\n <section\n className=\"rounded-2xl border border-border-seam bg-bg-surface p-5\"\n aria-labelledby=\"moderation-rule-heading\"\n >\n <h2\n id=\"moderation-rule-heading\"\n className=\"mb-1 flex items-center gap-2 text-lg font-semibold text-text-primary\"\n >\n <ShieldCheck className=\"h-5 w-5\" />\n {tr('comradecircle.moderation.rule.title', 'Auto-moderation rules')}\n </h2>\n <p className=\"mb-4 text-sm text-text-tertiary\">\n {canEdit\n ? tr(\n 'comradecircle.moderation.rule.explainer',\n 'Content matching a flagged term or spam pattern is queued for review. Auto-hide applies to spam only and can always be overturned.'\n )\n : tr(\n 'comradecircle.moderation.rule.readOnly',\n 'Only circle owners and admins can change these rules.'\n )}\n </p>\n\n {isLoading ? (\n <LoadingView label={tr('comradecircle.moderation.rule.loading', 'Loading rules…')} />\n ) : isError || !rule ? (\n <ErrorView\n title={tr('comradecircle.moderation.rule.errorTitle', 'Could not load the rules')}\n onRetry={() => refetch()}\n />\n ) : (\n // Re-mount on save so the form always starts from the stored rule.\n <RuleForm key={rule.updatedAt} rule={rule} circleId={circleId} canEdit={canEdit} />\n )}\n </section>\n );\n}\n\nfunction RuleForm({\n rule,\n circleId,\n canEdit,\n}: {\n rule: ComModerationRule;\n circleId: string;\n canEdit: boolean;\n}) {\n const tr = useTr();\n const update = useComUpdateModerationRule(circleId);\n const [enabled, setEnabled] = useState(rule.enabled);\n const [autoHideSpam, setAutoHideSpam] = useState(rule.autoHideSpam);\n const [flaggedTerms, setFlaggedTerms] = useState<string[]>(rule.flaggedTerms);\n const [patternsText, setPatternsText] = useState(rule.spamPatterns.join('\\n'));\n\n const spamPatterns = patternsText\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n const tooManyPatterns = spamPatterns.length > MAX_SPAM_PATTERNS;\n\n const save = () =>\n update\n .mutateAsync({ enabled, autoHideSpam, flaggedTerms, spamPatterns })\n .then(() =>\n notify.success(tr('comradecircle.moderation.rule.saved', 'Moderation rules saved'))\n )\n .catch((e) =>\n notify.error(\n tr('comradecircle.moderation.rule.saveError', 'Could not save the rules'),\n describeError(e)\n )\n );\n\n return (\n <form\n className=\"flex flex-col gap-5\"\n data-testid=\"moderation-rule-form\"\n onSubmit={(e) => {\n e.preventDefault();\n void save();\n }}\n >\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <Label htmlFor=\"rule-enabled\">\n {tr('comradecircle.moderation.rule.enabled', 'Apply circle rules')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.rule.enabledHint',\n 'Built-in spam, harassment and prompt-injection checks always run.'\n )}\n </p>\n </div>\n <Switch\n id=\"rule-enabled\"\n checked={enabled}\n onCheckedChange={setEnabled}\n disabled={!canEdit}\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"rule-terms\">\n {tr('comradecircle.moderation.rule.terms', 'Flagged terms')}\n </Label>\n <TagInput\n id=\"rule-terms\"\n value={flaggedTerms}\n onChange={canEdit ? setFlaggedTerms : () => undefined}\n max={MAX_FLAGGED_TERMS}\n placeholder={tr(\n 'comradecircle.moderation.rule.termsPlaceholder',\n 'Type a word or phrase and press Enter'\n )}\n />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"rule-patterns\">\n {tr('comradecircle.moderation.rule.patterns', 'Spam patterns (one per line)')}\n </Label>\n <Textarea\n id=\"rule-patterns\"\n value={patternsText}\n onChange={(e) => setPatternsText(e.target.value)}\n rows={4}\n disabled={!canEdit}\n className=\"font-mono text-sm\"\n placeholder={'promo-code-[0-9]{4}\\nhttps?://bit\\\\.ly/\\\\w+'}\n />\n <p\n className={\n tooManyPatterns ? 'text-xs text-status-error-text' : 'text-xs text-text-tertiary'\n }\n >\n {tr(\n 'comradecircle.moderation.rule.patternsHint',\n 'Case-insensitive regular expressions, up to {{max}}. Nested quantifiers, backreferences and lookarounds are rejected to keep triage fast.',\n { max: MAX_SPAM_PATTERNS }\n )}\n </p>\n </div>\n\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <Label htmlFor=\"rule-autohide\">\n {tr('comradecircle.moderation.rule.autoHide', 'Auto-hide matching spam posts')}\n </Label>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'comradecircle.moderation.rule.autoHideHint',\n 'Hidden posts stay in the review queue and can be restored. Harassment and toxicity always go to a human.'\n )}\n </p>\n </div>\n <Switch\n id=\"rule-autohide\"\n checked={autoHideSpam}\n onCheckedChange={setAutoHideSpam}\n disabled={!canEdit}\n />\n </div>\n\n {canEdit && (\n <div className=\"flex justify-end\">\n <Button\n type=\"submit\"\n disabled={update.isPending || tooManyPatterns}\n data-testid=\"moderation-rule-save\"\n >\n {update.isPending\n ? tr('comradecircle.moderation.rule.saving', 'Saving…')\n : tr('comradecircle.moderation.rule.save', 'Save rules')}\n </Button>\n </div>\n )}\n </form>\n );\n}\n"],"mappings":"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"}
|
|
@@ -9,9 +9,9 @@ import { formatNumber as a } from "../../utils/formatters.js";
|
|
|
9
9
|
import { EmptyState as o } from "../../components/EmptyState.js";
|
|
10
10
|
import { ErrorView as s } from "../../components/StateViews.js";
|
|
11
11
|
import { Pagination as c } from "../../components/Pagination.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { FilterTabs as l } from "../../components/FilterTabs.js";
|
|
13
|
+
import { EventCard as u } from "../../components/EventCard.js";
|
|
14
|
+
import { CreateEventDialog as d } from "../../components/CreateEventDialog.js";
|
|
15
15
|
import { useState as f } from "react";
|
|
16
16
|
import { CalendarDays as p, Plus as m } from "lucide-react";
|
|
17
17
|
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
@@ -93,7 +93,7 @@ function C() {
|
|
|
93
93
|
]
|
|
94
94
|
}),
|
|
95
95
|
children: [
|
|
96
|
-
/* @__PURE__ */ h(
|
|
96
|
+
/* @__PURE__ */ h(l, {
|
|
97
97
|
options: D,
|
|
98
98
|
value: C,
|
|
99
99
|
onChange: P,
|
|
@@ -124,7 +124,7 @@ function C() {
|
|
|
124
124
|
}),
|
|
125
125
|
/* @__PURE__ */ h("div", {
|
|
126
126
|
className: "grid gap-4 md:grid-cols-2",
|
|
127
|
-
children: M.map((e) => /* @__PURE__ */ h(
|
|
127
|
+
children: M.map((e) => /* @__PURE__ */ h(u, {
|
|
128
128
|
event: e,
|
|
129
129
|
onOpen: () => v("events/" + e.id)
|
|
130
130
|
}, e.id))
|
|
@@ -138,7 +138,7 @@ function C() {
|
|
|
138
138
|
})
|
|
139
139
|
]
|
|
140
140
|
}),
|
|
141
|
-
/* @__PURE__ */ h(
|
|
141
|
+
/* @__PURE__ */ h(d, {
|
|
142
142
|
open: T,
|
|
143
143
|
onOpenChange: E
|
|
144
144
|
})
|
|
@@ -485,4 +485,57 @@ export interface ComRecordTipIntentInput {
|
|
|
485
485
|
currency?: string | null;
|
|
486
486
|
message?: string | null;
|
|
487
487
|
}
|
|
488
|
+
export type ComModerationTargetType = 'COMMENT' | 'MEMBERSHIP' | 'POST';
|
|
489
|
+
export type ComModerationVerdictType = 'ALLOW' | 'ESCALATE' | 'FLAG' | 'HIDE';
|
|
490
|
+
export type ComModerationCategory = 'HARASSMENT' | 'NSFW' | 'OFF_TOPIC' | 'SELF_PROMO' | 'SPAM';
|
|
491
|
+
export type ComModerationReviewDecision = 'OVERTURN' | 'UPHOLD';
|
|
492
|
+
export interface ComModerationVerdict {
|
|
493
|
+
id: string;
|
|
494
|
+
workspaceId: string;
|
|
495
|
+
circleId?: string | null;
|
|
496
|
+
targetType: ComModerationTargetType;
|
|
497
|
+
targetId: string;
|
|
498
|
+
verdict: ComModerationVerdictType;
|
|
499
|
+
categories: ComModerationCategory[];
|
|
500
|
+
confidence: number;
|
|
501
|
+
rationale: string;
|
|
502
|
+
actedAutomatically: boolean;
|
|
503
|
+
injectionDetected: boolean;
|
|
504
|
+
reviewedBy?: string | null;
|
|
505
|
+
reviewedAt?: string | null;
|
|
506
|
+
reviewNote?: string | null;
|
|
507
|
+
overturned: boolean;
|
|
508
|
+
createdAt: string;
|
|
509
|
+
}
|
|
510
|
+
export interface ComModerationRule {
|
|
511
|
+
id: string;
|
|
512
|
+
workspaceId: string;
|
|
513
|
+
circleId: string;
|
|
514
|
+
enabled: boolean;
|
|
515
|
+
flaggedTerms: string[];
|
|
516
|
+
spamPatterns: string[];
|
|
517
|
+
toxicityThreshold: number;
|
|
518
|
+
autoHideSpam: boolean;
|
|
519
|
+
updatedAt: string;
|
|
520
|
+
}
|
|
521
|
+
export interface ComModerationRuleInput {
|
|
522
|
+
enabled?: boolean;
|
|
523
|
+
flaggedTerms?: string[];
|
|
524
|
+
spamPatterns?: string[];
|
|
525
|
+
autoHideSpam?: boolean;
|
|
526
|
+
}
|
|
527
|
+
export interface ComVerdictCount {
|
|
528
|
+
verdict: ComModerationVerdictType;
|
|
529
|
+
count: number;
|
|
530
|
+
}
|
|
531
|
+
export interface ComCircleActivitySummary {
|
|
532
|
+
periodStart: string;
|
|
533
|
+
periodEnd: string;
|
|
534
|
+
totalPosts: number;
|
|
535
|
+
totalComments: number;
|
|
536
|
+
newMembers: number;
|
|
537
|
+
activeMembers: number;
|
|
538
|
+
verdictCounts: ComVerdictCount[];
|
|
539
|
+
summaryText: string;
|
|
540
|
+
}
|
|
488
541
|
//# sourceMappingURL=index.d.ts.map
|