mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
59cab4d2aa
commit
e9b283ec80
2 changed files with 219 additions and 19 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue