feat: close #2449 edit / insert / delete messages modal

This commit is contained in:
Yidadaa 2023-07-21 00:24:26 +08:00
parent e5f6133127
commit 7c2fa9f8a4
5 changed files with 211 additions and 110 deletions

View File

@ -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 {

View File

@ -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>
); );
} }

View File

@ -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>
</> </>
); );

View File

@ -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",

View File

@ -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",