feat: add "Retry All Errors" batch reanalyze button
Backend: - POST /api/messages/reanalyze-batch — bulk reset error messages to pending - messages.repository.reanalyzeErrorBatch() — scoped by guildId/channelId/messageIds - messages.service.reanalyzeErrorBatch() — validation layer Frontend: - reanalyzeErrorBatch() API client function - useMessages.reanalyzeAllErrors() — optimistic state + batch call - MessagesPanel: destructive button visible when error count > 0 - Shows confirmation text with queued count Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d81aaf2b4a
commit
6da8a32c9b
@@ -207,6 +207,7 @@ export default function App() {
|
||||
guildName={monitorGuildName}
|
||||
messages={messages.messages}
|
||||
onReanalyze={messages.reanalyze}
|
||||
onReanalyzeAllErrors={messages.reanalyzeAllErrors}
|
||||
onLoadMore={messages.loadMore}
|
||||
hasMore={messages.hasMore}
|
||||
loadingMore={messages.loadingMore}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import type { MessageRecord } from "../../../shared/api/client";
|
||||
import { listMessages, reanalyzeMessage } from "../../../shared/api/client";
|
||||
import { listMessages, reanalyzeErrorBatch, reanalyzeMessage } from "../../../shared/api/client";
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
@@ -91,6 +91,29 @@ export function useMessages() {
|
||||
await reanalyzeMessage(id);
|
||||
}, []);
|
||||
|
||||
const reanalyzeAllErrors = useCallback(
|
||||
async (): Promise<number> => {
|
||||
// Optimistically mark all error messages as pending
|
||||
setMessages((prev) =>
|
||||
prev.map((message) =>
|
||||
message.ai_status === "error"
|
||||
? {
|
||||
...message,
|
||||
ai_status: "pending" as const,
|
||||
ai_error: null,
|
||||
ai_analysis: null,
|
||||
}
|
||||
: message,
|
||||
),
|
||||
);
|
||||
const { count } = await reanalyzeErrorBatch({
|
||||
guildId: currentGuild.current ?? undefined,
|
||||
});
|
||||
return count;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
messages,
|
||||
setMessages,
|
||||
@@ -99,6 +122,7 @@ export function useMessages() {
|
||||
error,
|
||||
fetchMessages,
|
||||
reanalyze,
|
||||
reanalyzeAllErrors,
|
||||
loadMore,
|
||||
hasMore,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Filter, Search, X } from "lucide-react";
|
||||
import { Filter, RotateCw, Search, X } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { MessageRecord } from "../../shared/api/client";
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ interface MessagesPanelProps {
|
||||
guildName: string | null;
|
||||
messages: MessageRecord[];
|
||||
onReanalyze: (id: string) => Promise<void>;
|
||||
onReanalyzeAllErrors?: () => Promise<number>;
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
loadingMore?: boolean;
|
||||
@@ -32,6 +33,7 @@ export function MessagesPanel({
|
||||
guildName,
|
||||
messages,
|
||||
onReanalyze,
|
||||
onReanalyzeAllErrors,
|
||||
onLoadMore,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
@@ -42,6 +44,8 @@ export function MessagesPanel({
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [aiFilter, setAiFilter] = useState<AiFilter>("all");
|
||||
const [viewTab, setViewTab] = useState<"all" | "images">("all");
|
||||
const [retryingAll, setRetryingAll] = useState(false);
|
||||
const [retriedCount, setRetriedCount] = useState<number | null>(null);
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
@@ -187,6 +191,36 @@ export function MessagesPanel({
|
||||
<X className="mr-1 h-3 w-3" /> Clear
|
||||
</Button>
|
||||
)}
|
||||
{stats.error > 0 && onReanalyzeAllErrors && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={retryingAll}
|
||||
onClick={async () => {
|
||||
setRetryingAll(true);
|
||||
setRetriedCount(null);
|
||||
try {
|
||||
const count = await onReanalyzeAllErrors();
|
||||
setRetriedCount(count);
|
||||
} finally {
|
||||
setRetryingAll(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCw
|
||||
className={`mr-1.5 h-3.5 w-3.5 ${retryingAll ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{retryingAll
|
||||
? "Retrying..."
|
||||
: `Retry All Errors (${stats.error})`}
|
||||
</Button>
|
||||
)}
|
||||
{retriedCount !== null && (
|
||||
<span className="text-xs text-green-400">
|
||||
{retriedCount} message{retriedCount !== 1 ? "s" : ""} queued for
|
||||
re-analysis
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
{(
|
||||
|
||||
@@ -167,6 +167,17 @@ export function reanalyzeMessage(id: string): Promise<void> {
|
||||
return request<void>(`/api/messages/${id}/reanalyze`, { method: "POST" });
|
||||
}
|
||||
|
||||
export function reanalyzeErrorBatch(opts: {
|
||||
guildId?: string;
|
||||
channelId?: string;
|
||||
messageIds?: string[];
|
||||
}): Promise<{ ok: boolean; count: number }> {
|
||||
return request<{ ok: boolean; count: number }>(
|
||||
"/api/messages/reanalyze-batch",
|
||||
{ method: "POST", body: JSON.stringify(opts) },
|
||||
);
|
||||
}
|
||||
|
||||
export function moderateMessage(
|
||||
id: string,
|
||||
actionType: string,
|
||||
|
||||
Reference in New Issue
Block a user