forked from XiaoMo/ChatGPT-Next-Web
feat: close #2449 edit / insert / delete messages modal
This commit is contained in:
parent
e5f6133127
commit
7c2fa9f8a4
@ -95,11 +95,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.context-prompt {
|
.context-prompt {
|
||||||
|
.context-prompt-insert {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
opacity: 0.2;
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.context-prompt-row {
|
.context-prompt-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.context-drag {
|
.context-drag {
|
||||||
|
@ -25,6 +25,8 @@ import SettingsIcon from "../icons/chat-settings.svg";
|
|||||||
import DeleteIcon from "../icons/clear.svg";
|
import DeleteIcon from "../icons/clear.svg";
|
||||||
import PinIcon from "../icons/pin.svg";
|
import PinIcon from "../icons/pin.svg";
|
||||||
import EditIcon from "../icons/rename.svg";
|
import EditIcon from "../icons/rename.svg";
|
||||||
|
import ConfirmIcon from "../icons/confirm.svg";
|
||||||
|
import CancelIcon from "../icons/cancel.svg";
|
||||||
|
|
||||||
import LightIcon from "../icons/light.svg";
|
import LightIcon from "../icons/light.svg";
|
||||||
import DarkIcon from "../icons/dark.svg";
|
import DarkIcon from "../icons/dark.svg";
|
||||||
@ -63,6 +65,7 @@ import { IconButton } from "./button";
|
|||||||
import styles from "./chat.module.scss";
|
import styles from "./chat.module.scss";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
Modal,
|
Modal,
|
||||||
Selector,
|
Selector,
|
||||||
@ -73,7 +76,7 @@ import {
|
|||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
|
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
|
||||||
import { Avatar } from "./emoji";
|
import { Avatar } from "./emoji";
|
||||||
import { MaskAvatar, MaskConfig } from "./mask";
|
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||||
import { useMaskStore } from "../store/mask";
|
import { useMaskStore } from "../store/mask";
|
||||||
import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
@ -520,6 +523,68 @@ export function ChatActions(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function EditMessageModal(props: { onClose: () => void }) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const [messages, setMessages] = useState(session.messages.slice());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.UI.Edit}
|
||||||
|
onClose={props.onClose}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
text={Locale.UI.Cancel}
|
||||||
|
icon={<CancelIcon />}
|
||||||
|
key="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
type="primary"
|
||||||
|
text={Locale.UI.Confirm}
|
||||||
|
icon={<ConfirmIcon />}
|
||||||
|
key="ok"
|
||||||
|
onClick={() => {
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.messages = messages),
|
||||||
|
);
|
||||||
|
props.onClose();
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Chat.EditMessage.Topic.Title}
|
||||||
|
subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={session.topic}
|
||||||
|
onInput={(e) =>
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.topic = e.currentTarget.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<ContextPrompts
|
||||||
|
context={messages}
|
||||||
|
updateContext={(updater) => {
|
||||||
|
const newMessages = messages.slice();
|
||||||
|
updater(newMessages);
|
||||||
|
setMessages(newMessages);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat() {
|
||||||
type RenderMessage = ChatMessage & { preview?: boolean };
|
type RenderMessage = ChatMessage & { preview?: boolean };
|
||||||
|
|
||||||
@ -710,22 +775,6 @@ export function Chat() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findLastUserIndex = (messageId: string) => {
|
|
||||||
// find last user input message
|
|
||||||
let lastUserMessageIndex: number | null = null;
|
|
||||||
for (let i = 0; i < session.messages.length; i += 1) {
|
|
||||||
const message = session.messages[i];
|
|
||||||
if (message.role === "user") {
|
|
||||||
lastUserMessageIndex = i;
|
|
||||||
}
|
|
||||||
if (message.id === messageId) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastUserMessageIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteMessage = (msgId?: string) => {
|
const deleteMessage = (msgId?: string) => {
|
||||||
chatStore.updateCurrentSession(
|
chatStore.updateCurrentSession(
|
||||||
(session) =>
|
(session) =>
|
||||||
@ -859,16 +908,6 @@ export function Chat() {
|
|||||||
|
|
||||||
const [showPromptModal, setShowPromptModal] = useState(false);
|
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||||
|
|
||||||
const renameSession = () => {
|
|
||||||
showPrompt(Locale.Chat.Rename, session.topic).then((newTopic) => {
|
|
||||||
if (newTopic && newTopic !== session.topic) {
|
|
||||||
chatStore.updateCurrentSession(
|
|
||||||
(session) => (session.topic = newTopic!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientConfig = useMemo(() => getClientConfig(), []);
|
const clientConfig = useMemo(() => getClientConfig(), []);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -919,6 +958,9 @@ export function Chat() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// edit / insert message modal
|
||||||
|
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat} key={session.id}>
|
<div className={styles.chat} key={session.id}>
|
||||||
<div className="window-header" data-tauri-drag-region>
|
<div className="window-header" data-tauri-drag-region>
|
||||||
@ -938,7 +980,7 @@ export function Chat() {
|
|||||||
<div className={`window-header-title ${styles["chat-body-title"]}`}>
|
<div className={`window-header-title ${styles["chat-body-title"]}`}>
|
||||||
<div
|
<div
|
||||||
className={`window-header-main-title ${styles["chat-body-main-title"]}`}
|
className={`window-header-main-title ${styles["chat-body-main-title"]}`}
|
||||||
onClickCapture={renameSession}
|
onClickCapture={() => setIsEditingMessage(true)}
|
||||||
>
|
>
|
||||||
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
||||||
</div>
|
</div>
|
||||||
@ -952,7 +994,7 @@ export function Chat() {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<RenameIcon />}
|
icon={<RenameIcon />}
|
||||||
bordered
|
bordered
|
||||||
onClick={renameSession}
|
onClick={() => setIsEditingMessage(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1170,6 +1212,14 @@ export function Chat() {
|
|||||||
{showExport && (
|
{showExport && (
|
||||||
<ExportMessageModal onClose={() => setShowExport(false)} />
|
<ExportMessageModal onClose={() => setShowExport(false)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isEditingMessage && (
|
||||||
|
<EditMessageModal
|
||||||
|
onClose={() => {
|
||||||
|
setIsEditingMessage(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -215,67 +215,58 @@ function ContextPromptItem(props: {
|
|||||||
const [focusingInput, setFocusingInput] = useState(false);
|
const [focusingInput, setFocusingInput] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={props.prompt.id || props.index.toString()} index={props.index}>
|
<div className={chatStyle["context-prompt-row"]}>
|
||||||
{(provided) => (
|
{!focusingInput && (
|
||||||
<div
|
<>
|
||||||
className={chatStyle["context-prompt-row"]}
|
<div className={chatStyle["context-drag"]}>
|
||||||
ref={provided.innerRef}
|
<DragIcon />
|
||||||
{...provided.draggableProps}
|
</div>
|
||||||
{...provided.dragHandleProps}
|
<Select
|
||||||
>
|
value={props.prompt.role}
|
||||||
{!focusingInput && (
|
className={chatStyle["context-role"]}
|
||||||
<>
|
onChange={(e) =>
|
||||||
<div className={chatStyle["context-drag"]}>
|
|
||||||
<DragIcon />
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
value={props.prompt.role}
|
|
||||||
className={chatStyle["context-role"]}
|
|
||||||
onChange={(e) =>
|
|
||||||
props.update({
|
|
||||||
...props.prompt,
|
|
||||||
role: e.target.value as any,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ROLES.map((r) => (
|
|
||||||
<option key={r} value={r}>
|
|
||||||
{r}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
value={props.prompt.content}
|
|
||||||
type="text"
|
|
||||||
className={chatStyle["context-content"]}
|
|
||||||
rows={focusingInput ? 5 : 1}
|
|
||||||
onFocus={() => setFocusingInput(true)}
|
|
||||||
onBlur={() => {
|
|
||||||
setFocusingInput(false);
|
|
||||||
// If the selection is not removed when the user loses focus, some
|
|
||||||
// extensions like "Translate" will always display a floating bar
|
|
||||||
window?.getSelection()?.removeAllRanges();
|
|
||||||
}}
|
|
||||||
onInput={(e) =>
|
|
||||||
props.update({
|
props.update({
|
||||||
...props.prompt,
|
...props.prompt,
|
||||||
content: e.currentTarget.value as any,
|
role: e.target.value as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
{!focusingInput && (
|
{ROLES.map((r) => (
|
||||||
<IconButton
|
<option key={r} value={r}>
|
||||||
icon={<DeleteIcon />}
|
{r}
|
||||||
className={chatStyle["context-delete-button"]}
|
</option>
|
||||||
onClick={() => props.remove()}
|
))}
|
||||||
bordered
|
</Select>
|
||||||
/>
|
</>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
<Input
|
||||||
|
value={props.prompt.content}
|
||||||
|
type="text"
|
||||||
|
className={chatStyle["context-content"]}
|
||||||
|
rows={focusingInput ? 5 : 1}
|
||||||
|
onFocus={() => setFocusingInput(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
setFocusingInput(false);
|
||||||
|
// If the selection is not removed when the user loses focus, some
|
||||||
|
// extensions like "Translate" will always display a floating bar
|
||||||
|
window?.getSelection()?.removeAllRanges();
|
||||||
|
}}
|
||||||
|
onInput={(e) =>
|
||||||
|
props.update({
|
||||||
|
...props.prompt,
|
||||||
|
content: e.currentTarget.value as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!focusingInput && (
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
className={chatStyle["context-delete-button"]}
|
||||||
|
onClick={() => props.remove()}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,8 +276,8 @@ export function ContextPrompts(props: {
|
|||||||
}) {
|
}) {
|
||||||
const context = props.context;
|
const context = props.context;
|
||||||
|
|
||||||
const addContextPrompt = (prompt: ChatMessage) => {
|
const addContextPrompt = (prompt: ChatMessage, i: number) => {
|
||||||
props.updateContext((context) => context.push(prompt));
|
props.updateContext((context) => context.splice(i, 0, prompt));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeContextPrompt = (i: number) => {
|
const removeContextPrompt = (i: number) => {
|
||||||
@ -319,13 +310,41 @@ export function ContextPrompts(props: {
|
|||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
{context.map((c, i) => (
|
{context.map((c, i) => (
|
||||||
<ContextPromptItem
|
<Draggable
|
||||||
|
draggableId={c.id || i.toString()}
|
||||||
index={i}
|
index={i}
|
||||||
key={c.id}
|
key={c.id}
|
||||||
prompt={c}
|
>
|
||||||
update={(prompt) => updateContextPrompt(i, prompt)}
|
{(provided) => (
|
||||||
remove={() => removeContextPrompt(i)}
|
<div
|
||||||
/>
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
<ContextPromptItem
|
||||||
|
index={i}
|
||||||
|
prompt={c}
|
||||||
|
update={(prompt) => updateContextPrompt(i, prompt)}
|
||||||
|
remove={() => removeContextPrompt(i)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={chatStyle["context-prompt-insert"]}
|
||||||
|
onClick={() => {
|
||||||
|
addContextPrompt(
|
||||||
|
createMessage({
|
||||||
|
role: "user",
|
||||||
|
content: "",
|
||||||
|
date: new Date().toLocaleString(),
|
||||||
|
}),
|
||||||
|
i + 1,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
@ -333,23 +352,26 @@ export function ContextPrompts(props: {
|
|||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
|
||||||
<div className={chatStyle["context-prompt-row"]}>
|
{props.context.length === 0 && (
|
||||||
<IconButton
|
<div className={chatStyle["context-prompt-row"]}>
|
||||||
icon={<AddIcon />}
|
<IconButton
|
||||||
text={Locale.Context.Add}
|
icon={<AddIcon />}
|
||||||
bordered
|
text={Locale.Context.Add}
|
||||||
className={chatStyle["context-prompt-button"]}
|
bordered
|
||||||
onClick={() =>
|
className={chatStyle["context-prompt-button"]}
|
||||||
addContextPrompt(
|
onClick={() =>
|
||||||
createMessage({
|
addContextPrompt(
|
||||||
role: "user",
|
createMessage({
|
||||||
content: "",
|
role: "user",
|
||||||
date: "",
|
content: "",
|
||||||
}),
|
date: "",
|
||||||
)
|
}),
|
||||||
}
|
props.context.length,
|
||||||
/>
|
)
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -18,6 +18,12 @@ const cn = {
|
|||||||
},
|
},
|
||||||
Chat: {
|
Chat: {
|
||||||
SubTitle: (count: number) => `共 ${count} 条对话`,
|
SubTitle: (count: number) => `共 ${count} 条对话`,
|
||||||
|
EditMessage: {
|
||||||
|
Topic: {
|
||||||
|
Title: "聊天主题",
|
||||||
|
SubTitle: "更改当前聊天主题",
|
||||||
|
},
|
||||||
|
},
|
||||||
Actions: {
|
Actions: {
|
||||||
ChatList: "查看消息列表",
|
ChatList: "查看消息列表",
|
||||||
CompressedHistory: "查看压缩后的历史 Prompt",
|
CompressedHistory: "查看压缩后的历史 Prompt",
|
||||||
|
@ -20,6 +20,12 @@ const en: LocaleType = {
|
|||||||
},
|
},
|
||||||
Chat: {
|
Chat: {
|
||||||
SubTitle: (count: number) => `${count} messages`,
|
SubTitle: (count: number) => `${count} messages`,
|
||||||
|
EditMessage: {
|
||||||
|
Topic: {
|
||||||
|
Title: "Topic",
|
||||||
|
SubTitle: "Change the current topic",
|
||||||
|
},
|
||||||
|
},
|
||||||
Actions: {
|
Actions: {
|
||||||
ChatList: "Go To Chat List",
|
ChatList: "Go To Chat List",
|
||||||
CompressedHistory: "Compressed History Memory Prompt",
|
CompressedHistory: "Compressed History Memory Prompt",
|
||||||
|
Loading…
Reference in New Issue
Block a user