diff --git a/src/app/api/messages/[messageId]/route.ts b/src/app/api/messages/[messageId]/route.ts index b9a79cf..6e17fd0 100644 --- a/src/app/api/messages/[messageId]/route.ts +++ b/src/app/api/messages/[messageId]/route.ts @@ -117,3 +117,48 @@ export async function GET(request: Request, { params }: RouteParams) { ); } } + +/** + * DELETE /api/messages/[messageId] - 删除消息 + * 用于重新生成功能:删除 AI 消息后重新生成 + */ +export async function DELETE(request: Request, { params }: RouteParams) { + try { + const { messageId } = await params; + + if (!messageId) { + return NextResponse.json( + { error: 'Message ID is required' }, + { status: 400 } + ); + } + + // 查找消息 + const existingMessage = await db.query.messages.findFirst({ + where: eq(messages.messageId, messageId), + }); + + if (!existingMessage) { + return NextResponse.json( + { error: 'Message not found' }, + { status: 404 } + ); + } + + // 删除消息 + await db + .delete(messages) + .where(eq(messages.messageId, messageId)); + + return NextResponse.json({ + success: true, + message: 'Message deleted successfully', + }); + } catch (error) { + console.error('Delete message error:', error); + return NextResponse.json( + { error: 'Failed to delete message' }, + { status: 500 } + ); + } +} diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 9f47244..0b4a05b 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -53,6 +53,7 @@ export default function ChatPage({ params }: PageProps) { sendMessage, stopGeneration, setInitialMessages, + regenerateMessage, } = useStreamChat(); // 当前选择的模型和工具 @@ -265,6 +266,30 @@ export default function ChatPage({ params }: PageProps) { setEnableThinking(!enableThinking); }; + // 重新生成 AI 消息 + const handleRegenerate = async (messageId: string) => { + if (isStreaming) return; // 正在生成时不允许重新生成 + + const result = await regenerateMessage(messageId); + if (!result) { + console.error('Failed to regenerate message'); + return; + } + + const { userMessage } = result; + + // 重新发送用户消息 + // 注意:历史消息中的图片已经是 base64 格式,保存在 uploadedImages 中 + // sendMessage 会重新添加用户消息和 AI 消息占位 + await sendMessage({ + conversationId: chatId, + message: userMessage.content, + model: selectedModelId, + tools: enabledTools, + enableThinking, + }); + }; + // 切换模型(持久化到数据库) const handleModelChange = async (modelId: string) => { if (!conversation || modelId === selectedModelId) return; @@ -506,6 +531,7 @@ export default function ChatPage({ params }: PageProps) { uploadedImages={message.uploadedImages} uploadedDocuments={message.uploadedDocuments} pyodideStatus={message.pyodideStatus} + onRegenerate={message.role === 'assistant' && !isStreaming ? handleRegenerate : undefined} /> )) )} diff --git a/src/components/features/MessageBubble.tsx b/src/components/features/MessageBubble.tsx index 90dc2be..33c8658 100644 --- a/src/components/features/MessageBubble.tsx +++ b/src/components/features/MessageBubble.tsx @@ -1,9 +1,10 @@ 'use client'; import { useState } from 'react'; -import { Copy, ThumbsUp, ThumbsDown, RefreshCw, ChevronDown, ChevronUp, Brain, Loader2, AlertCircle, Check, FileText, FileCode } from 'lucide-react'; +import { Copy, RefreshCw, ChevronDown, ChevronUp, Brain, Loader2, AlertCircle, Check, FileText, FileCode } from 'lucide-react'; import { Avatar } from '@/components/ui/Avatar'; import { AILogo } from '@/components/ui/AILogo'; +import { Tooltip } from '@/components/ui/Tooltip'; import { MarkdownRenderer } from '@/components/markdown/MarkdownRenderer'; import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult'; import { ImageLightbox } from '@/components/ui/ImageLightbox'; @@ -30,6 +31,8 @@ interface MessageBubbleProps { message: string; progress?: number; }; + /** 重新生成回调(仅对 AI 消息有效),传入消息 ID */ + onRegenerate?: (messageId: string) => void; } // 格式化文件大小 @@ -49,7 +52,7 @@ function getDocumentIcon(type: string) { return FileText; } -export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, uploadedImages, uploadedDocuments, pyodideStatus }: MessageBubbleProps) { +export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, uploadedImages, uploadedDocuments, pyodideStatus, onRegenerate }: MessageBubbleProps) { const isUser = message.role === 'user'; const [thinkingExpanded, setThinkingExpanded] = useState(false); const [copied, setCopied] = useState(false); @@ -158,8 +161,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err {/* 悬停显示复制按钮 */} - - - + + + + {/* 重新生成按钮 */} + {onRegenerate && ( + + + + )} {/* 免责声明 */} @@ -346,24 +357,6 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err ); } -interface ActionButtonProps { - icon: React.ComponentType<{ size?: number }>; - title: string; - onClick?: () => void; -} - -function ActionButton({ icon: Icon, title, onClick }: ActionButtonProps) { - return ( - - ); -} - /** * 工具调用结果显示组件 * 专门处理代码执行结果和图片显示 diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index a925810..9020254 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -527,6 +527,55 @@ export function useStreamChat() { setMessages(initialMessages); }, []); + /** + * 重新生成消息 + * @param assistantMessageId 要重新生成的 AI 消息 ID + * @returns 对应的用户消息信息,用于重新发送;如果找不到则返回 null + */ + const regenerateMessage = useCallback(async (assistantMessageId: string): Promise<{ + userMessage: ChatMessage; + } | null> => { + // 1. 在消息列表中找到该 AI 消息的索引 + const assistantIndex = messages.findIndex(m => m.id === assistantMessageId); + if (assistantIndex === -1) { + console.error('[regenerateMessage] AI message not found:', assistantMessageId); + return null; + } + + // 2. 找到对应的用户消息(AI 消息的前一条) + const userIndex = assistantIndex - 1; + if (userIndex < 0 || messages[userIndex].role !== 'user') { + console.error('[regenerateMessage] User message not found for AI message:', assistantMessageId); + return null; + } + + const userMessage = { ...messages[userIndex] }; + + // 3. 尝试从数据库删除 AI 消息(只有保存到数据库的消息才需要删除) + // 临时消息 ID 格式为 'assistant-timestamp',数据库消息 ID 是 nanoid 格式 + if (!assistantMessageId.startsWith('assistant-')) { + try { + const response = await fetch(`/api/messages/${assistantMessageId}`, { + method: 'DELETE', + }); + if (!response.ok) { + console.warn('[regenerateMessage] Failed to delete message from database:', await response.text()); + } + } catch (err) { + console.warn('[regenerateMessage] Error deleting message from database:', err); + } + } + + // 4. 从前端状态中移除用户消息、AI 消息及其后的所有消息 + // 因为 sendMessage 会重新添加用户消息和 AI 消息占位 + setMessages(prev => prev.slice(0, userIndex)); + + // 5. 返回用户消息信息 + return { + userMessage, + }; + }, [messages]); + return { messages, isStreaming, @@ -535,5 +584,6 @@ export function useStreamChat() { stopGeneration, clearMessages, setInitialMessages, + regenerateMessage, }; }