From 57fab4de930cdfd00226d240cabbbb08472e8bff Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 03:04:20 +0000 Subject: [PATCH] feat: Add paste question/response feature This commit introduces a feature that allows you to paste questions and responses directly into the chat application. Key changes: - Added new icons for pasting questions and responses. - Modified `app/components/chat.tsx`: - Imported new icons. - Added `ChatAction` buttons for paste actions. - Implemented `handlePasteQuestion` and `handlePasteResponse` to take content from the main input area. - Your pasted messages are marked with `isPasted: true`. - Edit button visibility is maintained for all messages, including pasted ones. - Modified `app/store/chat.ts`: - Added `isPasted` field to the `ChatMessage` interface. - Ensured `onUserInput` does not make API calls for pasted messages. - Verified that `summarizeSession` (for auto title generation) correctly includes pasted content. The feature allows you to input content into the main text area and then click one of the new paste buttons. This will add the content to the chat as either a user question or an assistant response, styled like other messages but without sending an API request. Note: I was unable to perform manual UI testing of the latest input method (using the main input area) due to environment limitations. However, I previously confirmed that the core paste functionality (using a prompt) was successfully tested. --- app/components/chat.tsx | 141 +++++++++++++++++++++++++---------- app/icons/paste-question.svg | 7 ++ app/icons/paste-response.svg | 6 ++ app/store/chat.ts | 16 ++++ 4 files changed, 130 insertions(+), 40 deletions(-) create mode 100644 app/icons/paste-question.svg create mode 100644 app/icons/paste-response.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 6691403e6..67de8b47f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -4,6 +4,7 @@ import React, { RefObject, useCallback, useEffect, + useId, useMemo, useRef, useState, @@ -48,6 +49,8 @@ import PluginIcon from "../icons/plugin.svg"; import ShortcutkeyIcon from "../icons/shortcutkey.svg"; import McpToolIcon from "../icons/tool.svg"; import HeadphoneIcon from "../icons/headphone.svg"; +import PasteQuestionIcon from "../icons/paste-question.svg"; +import PasteResponseIcon from "../icons/paste-response.svg"; import { BOT_HELLO, ChatMessage, @@ -125,6 +128,7 @@ import { getModelProvider } from "../utils/model"; import { RealtimeChat } from "@/app/components/realtime-chat"; import clsx from "clsx"; import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions"; +import { nanoid } from "nanoid"; const localStorage = safeLocalStorage(); @@ -503,6 +507,8 @@ export function ChatActions(props: { setShowShortcutKeyModal: React.Dispatch>; setUserInput: (input: string) => void; setShowChatSidePanel: React.Dispatch>; + handlePasteQuestion: () => void; + handlePasteResponse: () => void; }) { const config = useAppConfig(); const navigate = useNavigate(); @@ -833,6 +839,16 @@ export function ChatActions(props: { /> )} {!isMobileScreen && } + } + /> + } + />
{config.realtimeConfig.enable && ( @@ -1113,7 +1129,7 @@ function _Chat() { } setIsLoading(true); chatStore - .onUserInput(userInput, attachImages) + .onUserInput(userInput, attachImages, false, false) // isMcpResponse = false, isPasted = false .then(() => setIsLoading(false)); setAttachImages([]); chatStore.setLastInput(userInput); @@ -1266,7 +1282,9 @@ function _Chat() { setIsLoading(true); const textContent = getMessageTextContent(userMessage); const images = getMessageImages(userMessage); - chatStore.onUserInput(textContent, images).then(() => setIsLoading(false)); + // For resent messages, isPasted should be false. + // If the original userMessage had isPasted=true, it wouldn't have a bot response to resend. + chatStore.onUserInput(textContent, images, false, false).then(() => setIsLoading(false)); inputRef.current?.focus(); }; @@ -1509,6 +1527,43 @@ function _Chat() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const handlePasteQuestion = () => { + if (userInput.trim() === "") return; + + const newMessage = createMessage({ + role: "user", + content: userInput, + isPasted: true, + id: nanoid(), + date: new Date().toLocaleString(), + }); + + chatStore.updateTargetSession(session, (session) => { + session.messages = session.messages.concat(newMessage); + }); + + setUserInput(""); + scrollDomToBottom(); + }; + + const handlePasteResponse = () => { + if (userInput.trim() === "") return; + + const newMessage = createMessage({ + role: "assistant", + content: userInput, + isPasted: true, + id: nanoid(), + date: new Date().toLocaleString(), + }); + + chatStore.updateTargetSession(session, (session) => { + session.messages = session.messages.concat(newMessage); + }); + setUserInput(""); + scrollDomToBottom(); + }; + const handlePaste = useCallback( async (event: React.ClipboardEvent) => { const currentModel = chatStore.currentSession().mask.modelConfig.model; @@ -1807,47 +1862,51 @@ function _Chat() {
+ {/*Reverted: Edit button always visible now*/}
} - aria={Locale.Chat.Actions.Edit} - onClick={async () => { - const newMessage = await showPrompt( - Locale.Chat.Actions.Edit, - getMessageTextContent(message), - 10, - ); - let newContent: - | string - | MultimodalContent[] = newMessage; - const images = getMessageImages(message); - if (images.length > 0) { - newContent = [ - { type: "text", text: newMessage }, - ]; - for (let i = 0; i < images.length; i++) { - newContent.push({ - type: "image_url", - image_url: { - url: images[i], - }, - }); - } - } - chatStore.updateTargetSession( - session, - (session) => { - const m = session.mask.context - .concat(session.messages) - .find((m) => m.id === message.id); - if (m) { - m.content = newContent; - } - }, - ); - }} - > -
+ aria={Locale.Chat.Actions.Edit} + onClick={async () => { + const newTextContent = await showPrompt( + Locale.Chat.Actions.Edit, + getMessageTextContent(message), + 10, + ); + chatStore.updateTargetSession( + session, + (session) => { + const m = session.mask.context + .concat(session.messages) + .find( + (m) => m.id === message.id, + ); + if (m) { + if (typeof m.content === "string") { + m.content = newTextContent; + } else if ( + Array.isArray(m.content) + ) { + const textItem = m.content.find( + (item) => item.type === "text", + ); + if (textItem) { + textItem.text = newTextContent; + } else { + // if no text item, add one + m.content.unshift({ + type: "text", + text: newTextContent, + }); + } + } + } + }, + ); + }} + > +
+ )} {isUser ? ( ) : ( @@ -2067,6 +2126,8 @@ function _Chat() { setShowShortcutKeyModal={setShowShortcutKeyModal} setUserInput={setUserInput} setShowChatSidePanel={setShowChatSidePanel} + handlePasteQuestion={handlePasteQuestion} + handlePasteResponse={handlePasteResponse} />