mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-09-26 21:26:37 +08:00
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.
This commit is contained in:
parent
a4e504a93e
commit
57fab4de93
@ -4,6 +4,7 @@ import React, {
|
|||||||
RefObject,
|
RefObject,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useId,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -48,6 +49,8 @@ import PluginIcon from "../icons/plugin.svg";
|
|||||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||||
import McpToolIcon from "../icons/tool.svg";
|
import McpToolIcon from "../icons/tool.svg";
|
||||||
import HeadphoneIcon from "../icons/headphone.svg";
|
import HeadphoneIcon from "../icons/headphone.svg";
|
||||||
|
import PasteQuestionIcon from "../icons/paste-question.svg";
|
||||||
|
import PasteResponseIcon from "../icons/paste-response.svg";
|
||||||
import {
|
import {
|
||||||
BOT_HELLO,
|
BOT_HELLO,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@ -125,6 +128,7 @@ import { getModelProvider } from "../utils/model";
|
|||||||
import { RealtimeChat } from "@/app/components/realtime-chat";
|
import { RealtimeChat } from "@/app/components/realtime-chat";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
|
import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
@ -503,6 +507,8 @@ export function ChatActions(props: {
|
|||||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setUserInput: (input: string) => void;
|
setUserInput: (input: string) => void;
|
||||||
setShowChatSidePanel: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowChatSidePanel: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
handlePasteQuestion: () => void;
|
||||||
|
handlePasteResponse: () => void;
|
||||||
}) {
|
}) {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -833,6 +839,16 @@ export function ChatActions(props: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isMobileScreen && <MCPAction />}
|
{!isMobileScreen && <MCPAction />}
|
||||||
|
<ChatAction
|
||||||
|
onClick={props.handlePasteQuestion}
|
||||||
|
text={Locale.Chat.InputActions.PasteQuestion}
|
||||||
|
icon={<PasteQuestionIcon />}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
onClick={props.handlePasteResponse}
|
||||||
|
text={Locale.Chat.InputActions.PasteResponse}
|
||||||
|
icon={<PasteResponseIcon />}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
<div className={styles["chat-input-actions-end"]}>
|
<div className={styles["chat-input-actions-end"]}>
|
||||||
{config.realtimeConfig.enable && (
|
{config.realtimeConfig.enable && (
|
||||||
@ -1113,7 +1129,7 @@ function _Chat() {
|
|||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
chatStore
|
chatStore
|
||||||
.onUserInput(userInput, attachImages)
|
.onUserInput(userInput, attachImages, false, false) // isMcpResponse = false, isPasted = false
|
||||||
.then(() => setIsLoading(false));
|
.then(() => setIsLoading(false));
|
||||||
setAttachImages([]);
|
setAttachImages([]);
|
||||||
chatStore.setLastInput(userInput);
|
chatStore.setLastInput(userInput);
|
||||||
@ -1266,7 +1282,9 @@ function _Chat() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const textContent = getMessageTextContent(userMessage);
|
const textContent = getMessageTextContent(userMessage);
|
||||||
const images = getMessageImages(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();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1509,6 +1527,43 @@ function _Chat() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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(
|
const handlePaste = useCallback(
|
||||||
async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||||
@ -1807,47 +1862,51 @@ function _Chat() {
|
|||||||
<div className={styles["chat-message-container"]}>
|
<div className={styles["chat-message-container"]}>
|
||||||
<div className={styles["chat-message-header"]}>
|
<div className={styles["chat-message-header"]}>
|
||||||
<div className={styles["chat-message-avatar"]}>
|
<div className={styles["chat-message-avatar"]}>
|
||||||
|
{/*Reverted: Edit button always visible now*/}
|
||||||
<div className={styles["chat-message-edit"]}>
|
<div className={styles["chat-message-edit"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<EditIcon />}
|
icon={<EditIcon />}
|
||||||
aria={Locale.Chat.Actions.Edit}
|
aria={Locale.Chat.Actions.Edit}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const newMessage = await showPrompt(
|
const newTextContent = await showPrompt(
|
||||||
Locale.Chat.Actions.Edit,
|
Locale.Chat.Actions.Edit,
|
||||||
getMessageTextContent(message),
|
getMessageTextContent(message),
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
let newContent:
|
chatStore.updateTargetSession(
|
||||||
| string
|
session,
|
||||||
| MultimodalContent[] = newMessage;
|
(session) => {
|
||||||
const images = getMessageImages(message);
|
const m = session.mask.context
|
||||||
if (images.length > 0) {
|
.concat(session.messages)
|
||||||
newContent = [
|
.find(
|
||||||
{ type: "text", text: newMessage },
|
(m) => m.id === message.id,
|
||||||
];
|
);
|
||||||
for (let i = 0; i < images.length; i++) {
|
if (m) {
|
||||||
newContent.push({
|
if (typeof m.content === "string") {
|
||||||
type: "image_url",
|
m.content = newTextContent;
|
||||||
image_url: {
|
} else if (
|
||||||
url: images[i],
|
Array.isArray(m.content)
|
||||||
},
|
) {
|
||||||
});
|
const textItem = m.content.find(
|
||||||
}
|
(item) => item.type === "text",
|
||||||
}
|
);
|
||||||
chatStore.updateTargetSession(
|
if (textItem) {
|
||||||
session,
|
textItem.text = newTextContent;
|
||||||
(session) => {
|
} else {
|
||||||
const m = session.mask.context
|
// if no text item, add one
|
||||||
.concat(session.messages)
|
m.content.unshift({
|
||||||
.find((m) => m.id === message.id);
|
type: "text",
|
||||||
if (m) {
|
text: newTextContent,
|
||||||
m.content = newContent;
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
}
|
||||||
}}
|
},
|
||||||
></IconButton>
|
);
|
||||||
</div>
|
}}
|
||||||
|
></IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<Avatar avatar={config.avatar} />
|
<Avatar avatar={config.avatar} />
|
||||||
) : (
|
) : (
|
||||||
@ -2067,6 +2126,8 @@ function _Chat() {
|
|||||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||||
setUserInput={setUserInput}
|
setUserInput={setUserInput}
|
||||||
setShowChatSidePanel={setShowChatSidePanel}
|
setShowChatSidePanel={setShowChatSidePanel}
|
||||||
|
handlePasteQuestion={handlePasteQuestion}
|
||||||
|
handlePasteResponse={handlePasteResponse}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className={clsx(styles["chat-input-panel-inner"], {
|
className={clsx(styles["chat-input-panel-inner"], {
|
||||||
|
7
app/icons/paste-question.svg
Normal file
7
app/icons/paste-question.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
|
||||||
|
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
|
||||||
|
<circle cx="12" cy="13" r="1"></circle>
|
||||||
|
<path d="M12 16v-2.5"></path>
|
||||||
|
<path d="M12 9.5V10"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 450 B |
6
app/icons/paste-response.svg
Normal file
6
app/icons/paste-response.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
|
||||||
|
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
|
||||||
|
<path d="M12 16l-2-2h4l-2 2z"></path>
|
||||||
|
<path d="M12 10v4"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 414 B |
@ -63,6 +63,7 @@ export type ChatMessage = RequestMessage & {
|
|||||||
tools?: ChatMessageTool[];
|
tools?: ChatMessageTool[];
|
||||||
audio_url?: string;
|
audio_url?: string;
|
||||||
isMcpResponse?: boolean;
|
isMcpResponse?: boolean;
|
||||||
|
isPasted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
||||||
@ -408,6 +409,7 @@ export const useChatStore = createPersistStore(
|
|||||||
content: string,
|
content: string,
|
||||||
attachImages?: string[],
|
attachImages?: string[],
|
||||||
isMcpResponse?: boolean,
|
isMcpResponse?: boolean,
|
||||||
|
isPasted?: boolean, // Added isPasted parameter
|
||||||
) {
|
) {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const modelConfig = session.mask.modelConfig;
|
const modelConfig = session.mask.modelConfig;
|
||||||
@ -431,8 +433,22 @@ export const useChatStore = createPersistStore(
|
|||||||
role: "user",
|
role: "user",
|
||||||
content: mContent,
|
content: mContent,
|
||||||
isMcpResponse,
|
isMcpResponse,
|
||||||
|
isPasted, // Pass isPasted to createMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If message is pasted, add it to messages and return early.
|
||||||
|
if (userMessage.isPasted) {
|
||||||
|
get().updateTargetSession(session, (session) => {
|
||||||
|
session.messages = session.messages.concat([userMessage]);
|
||||||
|
session.lastUpdate = Date.now();
|
||||||
|
});
|
||||||
|
get().updateStat(userMessage, session);
|
||||||
|
// No bot message needed if user message is pasted and we are stopping here.
|
||||||
|
// Or, if it's a pasted response, both messages are created in chat.tsx.
|
||||||
|
// This early return prevents API call.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const botMessage: ChatMessage = createMessage({
|
const botMessage: ChatMessage = createMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
streaming: true,
|
streaming: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user