feat: 删除提问时级联删除其回答,并移除回答上的删除按钮
- 删除按钮只保留在用户提问气泡上;AI 回答不再单独提供删除
- 后端 DELETE /messages/{id} 改为按整轮删除:从提问删到下一条提问之前,
返回实际删除的 deleted_ids,保持 checkpoint 合法(无孤立工具结果/回答)
- 前端隐藏后端返回的整批 id,删除后即时消失
- 更新删除相关中英文案
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5d12616211
commit
1f2b1e3b8a
7 files changed
+73
-46
No files matched your search
@@ -17,10 +17,11 @@ import { useDeleteMessage } from "@/core/threads/hooks";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
/**
|
||||
* Hover affordance that deletes a single message (a user question or an
|
||||
* assistant answer) after a confirmation step. The actual removal is persisted
|
||||
* server-side; `onDeleted` lets the parent hide the bubble immediately without
|
||||
* waiting for the stream cache to refresh.
|
||||
* 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,
|
||||
@@ -30,7 +31,7 @@ export function DeleteMessageButton({
|
||||
}: {
|
||||
threadId: string;
|
||||
messageId: string;
|
||||
onDeleted?: () => void;
|
||||
onDeleted?: (deletedIds: string[]) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
@@ -39,8 +40,11 @@ export function DeleteMessageButton({
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
await deleteMessage.mutateAsync({ threadId, messageId });
|
||||
onDeleted?.();
|
||||
const deletedIds = await deleteMessage.mutateAsync({
|
||||
threadId,
|
||||
messageId,
|
||||
});
|
||||
onDeleted?.(deletedIds);
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
|
||||
@@ -147,18 +147,13 @@ export function MessageListItem({
|
||||
displaySources?: RuntimeDisplaySource[];
|
||||
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||
onEditResubmit?: (text: string) => void;
|
||||
onMessageDeleted?: (messageId: string) => void;
|
||||
onMessageDeleted?: (deletedIds: string[]) => void;
|
||||
}) {
|
||||
const isHuman = message.type === 'human';
|
||||
const messageMarkdown =
|
||||
extractContentFromMessage(message) ??
|
||||
extractReasoningContentFromMessage(message) ??
|
||||
'';
|
||||
// Deletion is offered only for messages that already exist server-side
|
||||
// (real, non-optimistic ids). Optimistic placeholders (`opt-*`) have no
|
||||
// checkpoint entry to remove.
|
||||
const canDelete =
|
||||
!!onMessageDeleted && !!message.id && !message.id.startsWith('opt-');
|
||||
return (
|
||||
<AIElementMessage
|
||||
className={cn(
|
||||
@@ -200,13 +195,6 @@ export function MessageListItem({
|
||||
fileName="message"
|
||||
/>
|
||||
)}
|
||||
{canDelete && (
|
||||
<DeleteMessageButton
|
||||
threadId={threadId}
|
||||
messageId={message.id!}
|
||||
onDeleted={() => onMessageDeleted?.(message.id!)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</MessageToolbar>
|
||||
)}
|
||||
@@ -389,7 +377,7 @@ function MessageContent_({
|
||||
displaySources?: RuntimeDisplaySource[];
|
||||
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||
onEditResubmit?: (text: string) => void;
|
||||
onMessageDeleted?: (messageId: string) => void;
|
||||
onMessageDeleted?: (deletedIds: string[]) => void;
|
||||
}) {
|
||||
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||
|
||||
@@ -521,6 +509,9 @@ function MessageContent_({
|
||||
}
|
||||
|
||||
if (isHuman) {
|
||||
// Only the user's own question carries a delete affordance, and only once
|
||||
// it exists server-side (real, non-optimistic id). Deleting it cascades to
|
||||
// its answer on the backend.
|
||||
const canDelete =
|
||||
!!onMessageDeleted && !!message.id && !message.id.startsWith('opt-');
|
||||
return (
|
||||
@@ -536,7 +527,7 @@ function MessageContent_({
|
||||
<DeleteMessageButton
|
||||
threadId={threadId}
|
||||
messageId={message.id!}
|
||||
onDeleted={() => onMessageDeleted?.(message.id!)}
|
||||
onDeleted={(ids) => onMessageDeleted?.(ids)}
|
||||
className="size-6"
|
||||
/>
|
||||
) : null
|
||||
|
||||
@@ -104,10 +104,11 @@ export function MessageList({
|
||||
useEffect(() => {
|
||||
setHiddenMessageIds(new Set());
|
||||
}, [threadId]);
|
||||
const handleMessageDeleted = useCallback((messageId: string) => {
|
||||
const handleMessageDeleted = useCallback((deletedIds: string[]) => {
|
||||
if (deletedIds.length === 0) return;
|
||||
setHiddenMessageIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(messageId);
|
||||
for (const id of deletedIds) next.add(id);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
Reference in new issue
Block a user