feat: surface AI analysis duration across gateway, backend, and FE
Adds per-message AI moderation analysis time (ai_analysis_duration_ms) so operators can see how long the LLM took to moderate each message. Gateway: - messagesTable: new ai_analysis_duration_ms (bigint) column. - AIAnalysisUpdate + buildAIAnalysisSet: carry analysisDurationMs through both single and bulk update paths. - ai-analysis-worker: measure wall-clock time around runModerationAnalysis and attach it to every result in the batch. Backend: - Mirror schema column; messageMapper maps ai_analysis_duration_ms; moderation-types + MappedMessage expose it. Frontend: - message.ts type gains ai_analysis_duration_ms. - AiBadge (messages view) shows 'status · 1.2s' when duration is present; analysis view badge mirrors the same formatting. DB: - scripts/add-ai-analysis-duration.sql (idempotent ADD COLUMN IF NOT EXISTS). No behavior change for moderation logic; null until new gateway build records values.
This commit is contained in:
@@ -18,6 +18,12 @@ function aiTone(
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
/** Human-readable analysis duration, e.g. 850ms / 1.2s / 3.4s. */
|
||||
function formatAnalysisDuration(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
export function AnalysisView() {
|
||||
const [query, setQuery] = useState("");
|
||||
const search = useMessageSearch(query, query.trim().length >= 2);
|
||||
@@ -99,7 +105,10 @@ export function AnalysisView() {
|
||||
</span>
|
||||
{m.ai_status && (
|
||||
<Badge tone={aiTone(m.ai_status)} className="ml-auto">
|
||||
{m.ai_status}
|
||||
{m.ai_analysis_duration_ms &&
|
||||
m.ai_analysis_duration_ms > 0
|
||||
? `${m.ai_status} · ${formatAnalysisDuration(m.ai_analysis_duration_ms)}`
|
||||
: m.ai_status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -174,7 +174,10 @@ export function MessagesView({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AiBadge status={m.ai_status} />
|
||||
<AiBadge
|
||||
status={m.ai_status}
|
||||
durationMs={m.ai_analysis_duration_ms}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -207,7 +210,13 @@ export function MessagesView({
|
||||
);
|
||||
}
|
||||
|
||||
function AiBadge({ status }: { status?: AiStatus | null }) {
|
||||
function AiBadge({
|
||||
status,
|
||||
durationMs,
|
||||
}: {
|
||||
status?: AiStatus | null;
|
||||
durationMs?: number | null;
|
||||
}) {
|
||||
if (!status) return null;
|
||||
const tone = aiTone(status);
|
||||
const icon =
|
||||
@@ -222,14 +231,25 @@ function AiBadge({ status }: { status?: AiStatus | null }) {
|
||||
) : (
|
||||
<AlertTriangle className="size-3" />
|
||||
);
|
||||
const label =
|
||||
durationMs && durationMs > 0
|
||||
? `${status} · ${formatDuration(durationMs)}`
|
||||
: status;
|
||||
|
||||
return (
|
||||
<Badge tone={tone} dot={status === "processing" || status === "pending"}>
|
||||
{icon}
|
||||
{status}
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
/** Human-readable analysis duration, e.g. 850ms / 1.2s / 3.4s. */
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
function MessageDetail({
|
||||
m,
|
||||
attachments,
|
||||
@@ -250,7 +270,10 @@ function MessageDetail({
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<AiBadge status={m.ai_status} />
|
||||
<AiBadge
|
||||
status={m.ai_status}
|
||||
durationMs={m.ai_analysis_duration_ms}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface MessageRecord {
|
||||
ai_recommended_action?: AiRecommendedAction | null;
|
||||
ai_error?: string | null;
|
||||
ai_analyzed_at?: number | null;
|
||||
ai_analysis_duration_ms?: number | null;
|
||||
/** Detail-only: number of past edits (message_edits snapshots) */
|
||||
edit_count?: number;
|
||||
/** Detail-only: previous content snapshots, newest first */
|
||||
|
||||
Reference in New Issue
Block a user