diff --git a/web/public/locales/en/views/chat.json b/web/public/locales/en/views/chat.json index 674e630a3..3bd79a9b7 100644 --- a/web/public/locales/en/views/chat.json +++ b/web/public/locales/en/views/chat.json @@ -8,5 +8,6 @@ "call": "Call", "result": "Result", "arguments": "Arguments:", - "response": "Response:" + "response": "Response:", + "send": "Send" } diff --git a/web/src/components/chat/ChatMessage.tsx b/web/src/components/chat/ChatMessage.tsx index 3e87a495e..689ee5f01 100644 --- a/web/src/components/chat/ChatMessage.tsx +++ b/web/src/components/chat/ChatMessage.tsx @@ -1,9 +1,12 @@ +import { useState, useEffect, useRef } from "react"; import ReactMarkdown from "react-markdown"; import { useTranslation } from "react-i18next"; import copy from "copy-to-clipboard"; import { toast } from "sonner"; -import { FaCopy } from "react-icons/fa"; +import { FaCopy, FaPencilAlt } from "react-icons/fa"; +import { FaArrowUpLong } from "react-icons/fa6"; import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, @@ -14,11 +17,35 @@ import { cn } from "@/lib/utils"; type MessageBubbleProps = { role: "user" | "assistant"; content: string; + messageIndex?: number; + onEditSubmit?: (messageIndex: number, newContent: string) => void; }; -export function MessageBubble({ role, content }: MessageBubbleProps) { +export function MessageBubble({ + role, + content, + messageIndex = 0, + onEditSubmit, +}: MessageBubbleProps) { const { t } = useTranslation(["views/chat", "common"]); const isUser = role === "user"; + const [isEditing, setIsEditing] = useState(false); + const [draftContent, setDraftContent] = useState(content); + const editInputRef = useRef(null); + + useEffect(() => { + setDraftContent(content); + }, [content]); + + useEffect(() => { + if (isEditing) { + editInputRef.current?.focus(); + editInputRef.current?.setSelectionRange( + editInputRef.current.value.length, + editInputRef.current.value.length, + ); + } + }, [isEditing]); const handleCopy = () => { const text = content?.trim() || ""; @@ -28,6 +55,69 @@ export function MessageBubble({ role, content }: MessageBubbleProps) { } }; + const handleEditClick = () => { + setDraftContent(content); + setIsEditing(true); + }; + + const handleEditSubmit = () => { + const trimmed = draftContent.trim(); + if (!trimmed || onEditSubmit == null) return; + onEditSubmit(messageIndex, trimmed); + setIsEditing(false); + }; + + const handleEditCancel = () => { + setDraftContent(content); + setIsEditing(false); + }; + + const handleEditKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleEditSubmit(); + } + if (e.key === "Escape") { + handleEditCancel(); + } + }; + + if (isUser && isEditing) { + return ( +
+