Files
deerflow-code/frontend-web/src/components/workspace/messages/delete-message-button.tsx
T
吉鑫升andClaude Opus 4.8 1f2b1e3b8a feat: 删除提问时级联删除其回答,并移除回答上的删除按钮
- 删除按钮只保留在用户提问气泡上;AI 回答不再单独提供删除
- 后端 DELETE /messages/{id} 改为按整轮删除:从提问删到下一条提问之前,
  返回实际删除的 deleted_ids,保持 checkpoint 合法(无孤立工具结果/回答)
- 前端隐藏后端返回的整批 id,删除后即时消失
- 更新删除相关中英文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:34:52 +08:00

100 lines
2.6 KiB
TypeScript

import { Trash2Icon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useI18n } from "@/core/i18n/hooks";
import { useDeleteMessage } from "@/core/threads/hooks";
import { Tooltip } from "../tooltip";
/**
* Hover affordance that deletes a conversation turn (a user question together
* with its answer) after a confirmation step. The actual removal is persisted
* server-side; `onDeleted` receives the ids the server removed so the parent
* can hide those bubbles immediately without waiting for the stream cache to
* refresh.
*/
export function DeleteMessageButton({
threadId,
messageId,
onDeleted,
className,
}: {
threadId: string;
messageId: string;
onDeleted?: (deletedIds: string[]) => void;
className?: string;
}) {
const { t } = useI18n();
const [open, setOpen] = useState(false);
const deleteMessage = useDeleteMessage();
const handleConfirm = async () => {
try {
const deletedIds = await deleteMessage.mutateAsync({
threadId,
messageId,
});
onDeleted?.(deletedIds);
setOpen(false);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "删除消息失败。",
);
}
};
return (
<>
<Tooltip content={t.inputBox.deleteMessage}>
<Button
size="icon-sm"
type="button"
variant="ghost"
className={className}
onClick={() => setOpen(true)}
>
<Trash2Icon size={12} />
</Button>
</Tooltip>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>{t.inputBox.deleteMessageConfirmTitle}</DialogTitle>
<DialogDescription>
{t.inputBox.deleteMessageConfirmDescription}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => setOpen(false)}
disabled={deleteMessage.isPending}
>
{t.common.cancel}
</Button>
<Button
type="button"
variant="destructive"
onClick={() => void handleConfirm()}
disabled={deleteMessage.isPending}
>
{t.common.delete}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}