fix(automod): flow real LLM analysis + descriptive fallback
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m2s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m25s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m40s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m2s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m25s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m40s
Root cause: ai-analysis-worker read llmResult.explanation and llmResult.toxicityScore — fields the LLM pipeline never produces (canonical AnalysisResult uses analysis/score). Every message fell back to the bare template "Tidak ada indikasi pelanggaran." and the stored score was always 0. - Map analysis/score correctly; fallback now quotes the message content - Prompt: ban generic analysis phrasing, require reply context - LLM context: include replied-to message content (metadata.reference) so the model can explain what the user is replying to - Frontend: show thread/channel names from metadata instead of raw IDs (message card, detail views, search overlay); detail panel now displays the ai_analysis text - Auto-delete log/DM include the descriptive analysis as the reason
This commit is contained in:
@@ -11,6 +11,7 @@ interface AiAnalysisPanelProps {
|
||||
categories?: string[] | string | null;
|
||||
action?: string | null;
|
||||
score?: number | null;
|
||||
analysis?: string | null;
|
||||
}
|
||||
|
||||
const severityColor: Record<string, string> = {
|
||||
@@ -29,6 +30,7 @@ export function AiAnalysisPanel({
|
||||
categories,
|
||||
action,
|
||||
score,
|
||||
analysis,
|
||||
}: AiAnalysisPanelProps) {
|
||||
if (!status || status === "pending") {
|
||||
return (
|
||||
@@ -93,6 +95,12 @@ export function AiAnalysisPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{analysis && (
|
||||
<p className="text-xs leading-relaxed text-text-secondary/90 border-l-2 border-glass-border pl-2">
|
||||
{analysis}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{action && action !== "none" && (
|
||||
<div className="text-xs">
|
||||
<span className="text-text-secondary/60">Recommended: </span>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { safeParseJsonArray } from "@/lib/format";
|
||||
import { safeParseJsonArray, getMessageChannelLabel } from "@/lib/format";
|
||||
import type { MessageRecord } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AiStatusBadge } from "./ai-status-badge";
|
||||
@@ -52,9 +52,16 @@ export function MessageCard({
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(msg.created_at).toLocaleString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span
|
||||
className="text-xs text-muted-foreground"
|
||||
title={
|
||||
msg.thread_id
|
||||
? `Thread ${getMessageChannelLabel(msg)} (${msg.thread_id.slice(0, 8)})`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Hash className="size-3 inline mr-0.5" />
|
||||
{msg.channel_id.slice(0, 8)}
|
||||
{getMessageChannelLabel(msg)}
|
||||
</span>
|
||||
<AiStatusBadge status={msg.ai_status} />
|
||||
{msg.ai_severity && msg.ai_severity !== "none" && (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, MessageSquare } from "lucide-react";
|
||||
import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
|
||||
import { GlassCard } from "@/components/glass/card";
|
||||
import { AttachmentsGrid } from "./attachments-grid";
|
||||
import { AiAnalysisPanel } from "./ai-analysis-panel";
|
||||
import { getMessageChannelLabel } from "@/lib/format";
|
||||
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
||||
|
||||
interface MessageDetailViewProps {
|
||||
@@ -25,7 +26,10 @@ export function MessageDetailView({ message, attachments, onBack }: MessageDetai
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<MessageSquare className="size-4 text-primary" />
|
||||
<span className="font-semibold text-sm text-text-primary">{message.username}</span>
|
||||
<span className="text-[10px] text-text-secondary/40 font-mono">{message.channel_id?.slice(0, 8)}</span>
|
||||
<span className="text-[10px] text-text-secondary/40 font-mono inline-flex items-center gap-1">
|
||||
{message.thread_id && <MessagesSquare className="size-3" />}
|
||||
{getMessageChannelLabel(message)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
@@ -49,6 +53,7 @@ export function MessageDetailView({ message, attachments, onBack }: MessageDetai
|
||||
categories={message.ai_categories}
|
||||
action={message.ai_recommended_action}
|
||||
score={message.ai_moderation_score}
|
||||
analysis={message.ai_analysis}
|
||||
/>
|
||||
</GlassCard>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, MessageSquare } from "lucide-react";
|
||||
import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
|
||||
import { GlassCard } from "@/components/glass/card";
|
||||
import { AttachmentsGrid } from "./attachments-grid";
|
||||
import { AiAnalysisPanel } from "./ai-analysis-panel";
|
||||
import { getMessageChannelLabel } from "@/lib/format";
|
||||
import type { AttachmentRecord, MessageRecord } from "@/lib/types";
|
||||
|
||||
interface MessageDetailProps {
|
||||
@@ -25,7 +26,10 @@ export function MessageDetail({ message, attachments, onBack }: MessageDetailPro
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<MessageSquare className="size-4 text-primary" />
|
||||
<span className="font-semibold text-sm text-text-primary">{message.username}</span>
|
||||
<span className="text-[10px] text-text-secondary/40 font-mono">{message.channel_id?.slice(0, 8)}</span>
|
||||
<span className="text-[10px] text-text-secondary/40 font-mono inline-flex items-center gap-1">
|
||||
{message.thread_id && <MessagesSquare className="size-3" />}
|
||||
{getMessageChannelLabel(message)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
@@ -49,6 +53,7 @@ export function MessageDetail({ message, attachments, onBack }: MessageDetailPro
|
||||
categories={message.ai_categories}
|
||||
action={message.ai_recommended_action}
|
||||
score={message.ai_moderation_score}
|
||||
analysis={message.ai_analysis}
|
||||
/>
|
||||
</GlassCard>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Search, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { messagesApi } from "@/lib/api";
|
||||
import { getMessageChannelLabel } from "@/lib/format";
|
||||
import type { MessageRecord } from "@/lib/types";
|
||||
|
||||
interface SearchOverlayProps {
|
||||
@@ -83,7 +84,7 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="font-medium text-text-primary">{msg.username}</span>
|
||||
<span className="text-text-secondary/40">{msg.channel_id?.slice(0, 8)}</span>
|
||||
<span className="text-text-secondary/40">{getMessageChannelLabel(msg)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary/80 line-clamp-1 mt-0.5">{msg.content}</p>
|
||||
</button>
|
||||
|
||||
@@ -43,3 +43,32 @@ export function safeParseJsonObject(
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a human-readable channel/thread label for a message.
|
||||
*
|
||||
* The channel name (and thread name, when the message lives in a thread)
|
||||
* is captured by the gateway into the message metadata JSON under
|
||||
* `metadata.channel.{channelName,threadName}`. Prefer names over raw IDs:
|
||||
* a thread message shows its thread name, otherwise the channel name,
|
||||
* falling back to a truncated channel ID only when names are unavailable.
|
||||
*/
|
||||
export function getMessageChannelLabel(msg: {
|
||||
channel_id?: string;
|
||||
metadata?: string | null;
|
||||
}): string {
|
||||
let channelName: string | undefined;
|
||||
let threadName: string | undefined;
|
||||
try {
|
||||
const m = JSON.parse(msg.metadata ?? "");
|
||||
const ch = m?.channel;
|
||||
channelName =
|
||||
typeof ch?.channelName === "string" ? ch.channelName : undefined;
|
||||
threadName = typeof ch?.threadName === "string" ? ch.threadName : undefined;
|
||||
} catch {
|
||||
// metadata malformed — fall through to ID fallback
|
||||
}
|
||||
if (threadName) return threadName;
|
||||
if (channelName) return channelName;
|
||||
return msg.channel_id?.slice(0, 8) ?? "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user