feat: add warn category for minor rule violations

- Add "warn" status between "clean" and "flagged" for minor violations
- Update AI analyzer system prompt with community rules and warn category
- Warn: profanity, OOT, tone issues - requires warning but not deletion
- Flagged: NSFW, illegal, hacking, scam, harassment, violence, SARA - requires review/deletion
- Update types to support warn status in MessageRecord and AIAnalysisUpdate
- Update client UI to show three panels: All Messages, Warned, Flagged
- Warned messages show in right-top panel for quick review
- Flagged messages show in right-bottom panel for moderation action

This resolves:
- Need to distinguish between minor and severe violations
- Moderators can now warn users before taking action
- Better moderation workflow with three-tier system

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MythEclipseandClaude Opus 4.7 committed 2026-05-14 04:23:11 +07:00
1 parent 54a4096323
commit 93eb2303c7
4 files changed
+37 -16

No files matched your search

+3 -3
View File
@@ -47,7 +47,7 @@
<div class="content-card" style="margin-top:18px"><div class="card-title"><h2>Participants</h2><span class="mini">speaking now</span></div><div id="userList" class="participants"></div></div>
</section>
<section id="text" class="tab-content"><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;height:100%"><div class="content-card"><div class="card-title"><h2>All Messages</h2><span class="mini">all captures</span></div><div id="textList" class="feed"><div class="empty">Select channel to view text captures</div></div></div><div class="content-card"><div class="card-title"><h2>Flagged</h2><span class="mini">AI flagged only</span></div><div id="flaggedList" class="feed"><div class=Line truncated
<section id="text" class="tab-content"><div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;height:100%"><div class="content-card"><div class="card-title"><h2>All Messages</h2><span class="mini">all captures</span></div><div id="textList" class="feed"><div class="empty">Select channel to view text captures</div></div></div><div style="display:grid;grid-template-rows:1fr 1fr;gap:16px"><div class="content-card"><div class="card-title"><h2>Warned</h2><span class="mini">minor violatiLine truncated
</main>
<script>
@@ -71,7 +71,7 @@
const SAMPLE_RATE = 24000;
const CHANNELS = 1;
const el = {
wsDot: document.getElementById('wsDot'), wsStatusText: document.getElementById('wsStatusText'), activeTabLabel: document.getElementById('activeTabLabel'), errorBox: document.getElementById('errorBox'), guildSelect: document.getElementById('guildSelect'), channelSelect: document.getElementById('channelSelect'), channelFilter: document.getElementById('channelFilter'), joinVoiceBtn: document.getElementById('joinVoiceBtn'), disconnectVoiceBtn: document.getElementById('disconnectVoiceBtn'), voLine truncated
wsDot: document.getElementById('wsDot'), wsStatusText: document.getElementById('wsStatusText'), activeTabLabel: document.getElementById('activeTabLabel'), errorBox: document.getElementById('errorBox'), guildSelect: document.getElementById('guildSelect'), channelSelect: document.getElementById('channelSelect'), channelFilter: document.getElementById('channelFilter'), joinVoiceBtn: document.getElementById('joinVoiceBtn'), disconnectVoiceBtn: document.getElementById('disconnectVoiceBtn'), voLine truncated
};
for (let i = 0; i < 32; i++) { const bar = document.createElement('div'); bar.className = 'bar'; el.visualizer.appendChild(bar); }
const bars = [...document.querySelectorAll('.bar')];
@@ -130,7 +130,7 @@
function renderUsers(users) { el.userList.replaceChildren(); if (users.length === 0) return appendEmpty(el.userList, 'No active speakers'); for (const user of users) { const row = document.createElement('div'); row.className = `user-item${user.speaking ? ' speaking' : ''}`; const img = document.createElement('img'); img.src = user.avatar || ''; img.alt = ''; const name = document.createElement('span'); name.textContent = user.username; row.append(img, name); el.userList.appendChild(row); } Line truncated
async function fetchText() { if (!state.selectedTextChannel) return renderText(); const result = await apiRequest(`/api/messages?channel=${encodeURIComponent(state.selectedTextChannel)}&type=text&limit=80`); state.text = result.data || []; renderText(); }
function renderText() { el.textList.replaceChildren(); el.flaggedList.replaceChildren(); if (!state.selectedTextChannel) { appendEmpty(el.textList, 'Select channel to view text captures'); appendEmpty(el.flaggedList, 'No flagged messages'); return; } if (state.text.length === 0) { appendEmpty(el.textList, 'No text captures yet'); appendEmpty(el.flaggedList, 'No flagged messages'); return; } const flaggedMessages = []; for (const msg of state.text) { const metadata = parseMetadata(msg.metadaLine truncated
function renderText() { el.textList.replaceChildren(); el.warnedList.replaceChildren(); el.flaggedList.replaceChildren(); if (!state.selectedTextChannel) { appendEmpty(el.textList, 'Select channel to view text captures'); appendEmpty(el.warnedList, 'No warned messages'); appendEmpty(el.flaggedList, 'No flagged messages'); return; } if (state.text.length === 0) { appendEmpty(el.textList, 'No text captures yet'); appendEmpty(el.warnedList, 'No warned messages'); appendEmpty(el.flaggedList, 'NLine truncated
function appendAIAnalysis(card, msg) { const status = msg.ai_status || 'pending'; const wrap = document.createElement('div'); wrap.className = 'badges'; const badge = document.createElement('span'); badge.className = `badge ${status === 'flagged' ? 'delete' : status === 'clean' ? 'edit' : ''}`; badge.textContent = `AI: ${status}`; wrap.appendChild(badge); if (msg.ai_moderation_flags) { const flags = document.createElement('span'); flags.className = 'badge delete'; try { flags.textContent = Line truncated
function appendMedia(card, metadata) { const stickers = document.createElement('div'); stickers.className = 'sticker-strip'; for (const sticker of metadata.stickers || []) { const img = document.createElement('img'); img.className = 'sticker-img'; img.src = sticker.url; img.alt = sticker.name; stickers.appendChild(img); } if (stickers.childElementCount) card.appendChild(stickers); const embeds = document.createElement('div'); embeds.className = 'feed'; for (const embed of metadata.embeds ||Line truncated