feat: Add retry and edit functionality to chat messages

- Added edit button to user messages with hover effect
- Added retry button to assistant messages to regenerate responses
- Implemented edit mode with save & resend and cancel buttons
- Added state management for editing messages
- Updated both ChatUI and MessageDisplay components
- Edit functionality allows users to modify their messages and resend
- Retry functionality regenerates assistant responses from user message

Co-authored-by: ishaan <ishaan@berri.ai>
This commit is contained in:
Cursor Agent 2026-02-03 21:41:02 +00:00
parent 59cab4d2aa
commit e9b283ec80
2 changed files with 219 additions and 19 deletions

View file

@ -3,16 +3,20 @@
import {
ApiOutlined,
ArrowUpOutlined,
CheckOutlined,
CloseOutlined,
ClearOutlined,
CodeOutlined,
DatabaseOutlined,
DeleteOutlined,
EditOutlined,
FilePdfOutlined,
InfoCircleOutlined,
KeyOutlined,
LinkOutlined,
LoadingOutlined,
PictureOutlined,
RedoOutlined,
RobotOutlined,
SafetyOutlined,
SettingOutlined,
@ -222,6 +226,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
const [temperature, setTemperature] = useState<number>(1.0);
const [maxTokens, setMaxTokens] = useState<number>(2048);
const [useAdvancedParams, setUseAdvancedParams] = useState<boolean>(false);
const [editingMessageIndex, setEditingMessageIndex] = useState<number | null>(null);
const [editedMessageContent, setEditedMessageContent] = useState<string>("");
// Code Interpreter state (using custom hook)
const codeInterpreter = useCodeInterpreter();
@ -769,6 +775,68 @@ const ChatUI: React.FC<ChatUIProps> = ({
setUploadedAudio(null);
};
const handleEditMessage = (index: number) => {
const message = chatHistory[index];
if (message.role !== "user") return;
setEditingMessageIndex(index);
setEditedMessageContent(typeof message.content === "string" ? message.content : "");
};
const handleCancelEdit = () => {
setEditingMessageIndex(null);
setEditedMessageContent("");
};
const handleSaveEdit = async () => {
if (editingMessageIndex === null || editedMessageContent.trim() === "") return;
// Update the message content
const updatedHistory = [...chatHistory];
updatedHistory[editingMessageIndex] = {
...updatedHistory[editingMessageIndex],
content: editedMessageContent,
};
// Remove all messages after the edited message (including assistant response)
const newHistory = updatedHistory.slice(0, editingMessageIndex + 1);
setChatHistory(newHistory);
// Clear editing state
setEditingMessageIndex(null);
setEditedMessageContent("");
// Resend the message
setInputMessage(editedMessageContent);
// Use setTimeout to ensure state is updated before sending
setTimeout(() => {
handleSendMessage();
}, 100);
};
const handleRetryMessage = async (index: number) => {
// Find the corresponding user message
const userMessageIndex = index - 1;
if (userMessageIndex < 0 || chatHistory[userMessageIndex].role !== "user") return;
// Remove the assistant message and all subsequent messages
const newHistory = chatHistory.slice(0, userMessageIndex + 1);
setChatHistory(newHistory);
// Get the user message content
const userMessage = chatHistory[userMessageIndex];
const messageContent = typeof userMessage.content === "string" ? userMessage.content : "";
// Resend the message
setInputMessage(messageContent);
// Use setTimeout to ensure state is updated before sending
setTimeout(() => {
handleSendMessage();
}, 100);
};
const handleSendMessage = async () => {
if (inputMessage.trim() === "" && endpointType !== EndpointType.TRANSCRIPTION) return;
@ -1677,9 +1745,9 @@ const ChatUI: React.FC<ChatUIProps> = ({
{chatHistory.map((message, index) => (
<div key={index}>
<div className={`mb-4 ${message.role === "user" ? "text-right" : "text-left"}`}>
<div className={`mb-4 ${message.role === "user" ? "text-right" : "text-left"} group`}>
<div
className="inline-block max-w-[80%] rounded-lg shadow-sm p-3.5 px-4"
className="inline-block max-w-[80%] rounded-lg shadow-sm p-3.5 px-4 relative"
style={{
backgroundColor: message.role === "user" ? "#f0f8ff" : "#ffffff",
border: message.role === "user" ? "1px solid #e6f0fa" : "1px solid #f0f0f0",
@ -1706,6 +1774,34 @@ const ChatUI: React.FC<ChatUIProps> = ({
</span>
)}
</div>
{/* Action buttons - show on hover */}
{!isLoading && editingMessageIndex !== index && (
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
{message.role === "user" && (
<Tooltip title="Edit message">
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => handleEditMessage(index)}
className="hover:bg-blue-100 hover:text-blue-600"
/>
</Tooltip>
)}
{message.role === "assistant" && index > 0 && chatHistory[index - 1].role === "user" && (
<Tooltip title="Regenerate response">
<Button
type="text"
size="small"
icon={<RedoOutlined />}
onClick={() => handleRetryMessage(index)}
className="hover:bg-green-100 hover:text-green-600"
/>
</Tooltip>
)}
</div>
)}
{message.reasoningContent && <ReasoningContent reasoningContent={message.reasoningContent} />}
{/* Show MCP events at the start of assistant messages */}
@ -1736,16 +1832,45 @@ const ChatUI: React.FC<ChatUIProps> = ({
/>
)}
<div
className="whitespace-pre-wrap break-words max-w-full message-content"
style={{
wordWrap: "break-word",
overflowWrap: "break-word",
wordBreak: "break-word",
hyphens: "auto",
}}
>
{message.isImage ? (
{/* Edit mode UI */}
{editingMessageIndex === index ? (
<div className="space-y-2">
<TextArea
value={editedMessageContent}
onChange={(e) => setEditedMessageContent(e.target.value)}
autoSize={{ minRows: 2, maxRows: 10 }}
className="w-full"
/>
<div className="flex gap-2">
<Button
type="primary"
size="small"
icon={<CheckOutlined />}
onClick={handleSaveEdit}
className="bg-blue-600 hover:bg-blue-700"
>
Save & Resend
</Button>
<Button
size="small"
icon={<CloseOutlined />}
onClick={handleCancelEdit}
>
Cancel
</Button>
</div>
</div>
) : (
<div
className="whitespace-pre-wrap break-words max-w-full message-content"
style={{
wordWrap: "break-word",
overflowWrap: "break-word",
wordBreak: "break-word",
hyphens: "auto",
}}
>
{message.isImage ? (
<img
src={typeof message.content === "string" ? message.content : ""}
alt="Generated image"
@ -1836,7 +1961,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
totalLatency={message.totalLatency}
/>
)}
</div>
</div>
)}
</div>
</div>
</div>

View file

@ -1,24 +1,50 @@
import { Bot, Loader2, UserRound } from "lucide-react";
import React from "react";
import { Bot, Loader2, UserRound, Edit2, RotateCw } from "lucide-react";
import React, { useState } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Button, Input } from "antd";
import ChatImageRenderer from "../../chat_ui/ChatImageRenderer";
import ReasoningContent from "../../chat_ui/ReasoningContent";
import ResponseMetrics from "../../chat_ui/ResponseMetrics";
import { SearchResultsDisplay } from "../../chat_ui/SearchResultsDisplay";
import type { MessageType } from "../../chat_ui/types";
const { TextArea } = Input;
interface MessageDisplayProps {
messages: MessageType[];
isLoading: boolean;
onEditMessage?: (index: number, newContent: string) => void;
onRetryMessage?: (index: number) => void;
}
export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
export function MessageDisplay({ messages, isLoading, onEditMessage, onRetryMessage }: MessageDisplayProps) {
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editedContent, setEditedContent] = useState<string>("");
if (messages.length === 0) {
return <div className="h-full" />;
}
const handleStartEdit = (index: number, content: string) => {
setEditingIndex(index);
setEditedContent(typeof content === "string" ? content : "");
};
const handleSaveEdit = (index: number) => {
if (onEditMessage && editedContent.trim()) {
onEditMessage(index, editedContent);
}
setEditingIndex(null);
setEditedContent("");
};
const handleCancelEdit = () => {
setEditingIndex(null);
setEditedContent("");
};
const conversationBlocks: Array<{
user?: MessageType;
assistant?: MessageType;
@ -105,21 +131,57 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
return (
<div key={blockIndex} className="space-y-4">
{block.user && (
<div className="space-y-2 min-w-0">
<div className="space-y-2 min-w-0 group relative">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600">
<UserRound size={16} />
</div>
<div className="text-sm font-semibold text-gray-700">You</div>
{!isLoading && editingIndex === null && onEditMessage && block.user && (
<button
onClick={() => {
const userIndex = messages.findIndex((m) => m === block.user);
const content = typeof block.user?.content === "string" ? block.user.content : "";
handleStartEdit(userIndex, content);
}}
className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-blue-50 text-blue-600"
title="Edit message"
>
<Edit2 size={14} />
</button>
)}
</div>
{renderMessageBody(block.user)}
{editingIndex === messages.findIndex((m) => m === block.user) ? (
<div className="space-y-2">
<TextArea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
autoSize={{ minRows: 2, maxRows: 10 }}
className="w-full"
/>
<div className="flex gap-2">
<Button
type="primary"
size="small"
onClick={() => handleSaveEdit(messages.findIndex((m) => m === block.user))}
>
Save & Resend
</Button>
<Button size="small" onClick={handleCancelEdit}>
Cancel
</Button>
</div>
</div>
) : (
renderMessageBody(block.user)
)}
</div>
)}
<div className="border-t border-gray-200" />
{assistantMessage ? (
<div className="space-y-3 min-w-0">
<div className="space-y-3 min-w-0 group relative">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600">
<Bot size={16} />
@ -132,6 +194,18 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
</span>
)}
</div>
{!isLoading && editingIndex === null && onRetryMessage && block.user && (
<button
onClick={() => {
const assistantIndex = messages.findIndex((m) => m === assistantMessage);
onRetryMessage(assistantIndex);
}}
className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-green-50 text-green-600"
title="Regenerate response"
>
<RotateCw size={14} />
</button>
)}
</div>
{assistantMessage.reasoningContent && (
<ReasoningContent reasoningContent={assistantMessage.reasoningContent} />