diff --git a/app/components/home.module.scss b/app/components/home.module.scss index cef1662b5..15a2af240 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -252,6 +252,11 @@ right: 10px; pointer-events: all; } + .chat-message-top-left-actions { + opacity: 1; + left:10px; + pointer-events: all; + } } } @@ -284,6 +289,7 @@ .chat-message-item { box-sizing: border-box; max-width: 100%; + min-width: 70px; margin-top: 10px; border-radius: 10px; background-color: rgba(0, 0, 0, 0.05); @@ -324,6 +330,33 @@ } } +.chat-message-top-left-actions { + font-size: 12px; + position: absolute; + top: -26px; + transition: all ease 0.3s; + opacity: 0; + pointer-events: none; + + display: flex; + flex-direction: row-reverse; + + .chat-message-top-left-action { + opacity: 0.5; + color: var(--black); + white-space: nowrap; + cursor: pointer; + + &:hover { + opacity: 1; + } + + &:not(:first-child) { + margin-right: 10px; + } + } +} + .chat-message-user > .chat-message-container > .chat-message-item { background-color: var(--second); } @@ -341,6 +374,21 @@ color: #aaa; } +.chat-message-action-edit-button { + width: 24px; + height: 15px; + opacity: 1; + background: rgba(255, 255, 255, 1); + border: 1px solid rgba(29.044500000000014, 147.21574999999993, 170.85, 1); + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + &:not(:first-child) { + margin-right: 5px; + } +} + .chat-input-panel { position: absolute; bottom: 0px; diff --git a/app/components/home.tsx b/app/components/home.tsx index 2f09aa273..6044fba99 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -20,6 +20,8 @@ import MenuIcon from "../icons/menu.svg"; import CloseIcon from "../icons/close.svg"; import CopyIcon from "../icons/copy.svg"; import DownloadIcon from "../icons/download.svg"; +import OkIcon from "../icons/ok.svg"; +import ErrorIcon from "../icons/error.svg"; import { Message, SubmitKey, useChatStore, ChatSession } from "../store"; import { showModal, showToast } from "./ui-lib"; @@ -190,6 +192,13 @@ export function Chat(props: { const fontSize = useChatStore((state) => state.config.fontSize); const inputRef = useRef(null); + const MessageInputRefs = useRef([]); + // avoid rendered more hooks error + const setMessageInputRef = (element: HTMLDivElement | null, index: number) => { + if (element) { + MessageInputRefs.current[index] = element; + } + }; const [userInput, setUserInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); @@ -288,6 +297,18 @@ export function Chat(props: { } }; + const confirmEdit = (index: number, content: string) => { + chatStore.onConfirmEdit(index, content); + } + + const cancelEdit = (index: Message) => { + chatStore.onCancelEdit(index); + } + + const onEdit = (message: Message) => { + chatStore.onUserEdit(message); + } + // for auto-scroll const latestMessageRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); @@ -302,6 +323,7 @@ export function Chat(props: { content: "……", date: new Date().toLocaleString(), preview: true, + isEditing: false }, ] : [], @@ -314,6 +336,7 @@ export function Chat(props: { content: userInput, date: new Date().toLocaleString(), preview: true, + isEditing: false }, ] : [], @@ -400,7 +423,6 @@ export function Chat(props: {
{messages.map((message, i) => { const isUser = message.role === "user"; - return (
)} + {isUser && !message.preview && !message.isEditing && ( +
+
onEdit(message)}>, + {Locale.Chat.Actions.Edit} +
+
+ ) + } {(message.preview || message.content.length === 0) && !isUser ? ( @@ -455,7 +487,19 @@ export function Chat(props: { onContextMenu={(e) => onRightClick(e, message)} onDoubleClickCapture={() => setUserInput(message.content)} > - + {message.isEditing ? ( +
setMessageInputRef(element, i)} + contentEditable={true} + suppressContentEditableWarning={true} + style={{ outline: "0px solid transparent"}} + > + {message.content} +
+ ) : ( + + )}
)} @@ -466,6 +510,16 @@ export function Chat(props: { )} + {isUser && message.isEditing && ( +
+
{confirmEdit(i, MessageInputRefs.current[i].innerText!!)}}> + +
+
{cancelEdit(message)}}> + +
+
+ )} ); diff --git a/app/icons/error.svg b/app/icons/error.svg new file mode 100644 index 000000000..b5f00df02 --- /dev/null +++ b/app/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/ok.svg b/app/icons/ok.svg new file mode 100644 index 000000000..c1c1e64c9 --- /dev/null +++ b/app/icons/ok.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 737ccad45..5524668e5 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -17,6 +17,7 @@ const cn = { Copy: "复制", Stop: "停止", Retry: "重试", + Edit: "编辑" }, Rename: "重命名对话", Typing: "正在输入…", diff --git a/app/locales/en.ts b/app/locales/en.ts index 156c03616..5fad4514d 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -19,6 +19,7 @@ const en: LocaleType = { Copy: "Copy", Stop: "Stop", Retry: "Retry", + Edit: "Edit", }, Rename: "Rename Chat", Typing: "Typing…", diff --git a/app/locales/es.ts b/app/locales/es.ts index 3f7ad1bc7..025ace8f8 100644 --- a/app/locales/es.ts +++ b/app/locales/es.ts @@ -19,6 +19,7 @@ const es: LocaleType = { Copy: "Copiar", Stop: "Detener", Retry: "Reintentar", + Edit: "Editar", }, Rename: "Renombrar chat", Typing: "Escribiendo...", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index cfba7add5..3d0e03aab 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -18,6 +18,7 @@ const tw: LocaleType = { Copy: "複製", Stop: "停止", Retry: "重試", + Edit: "編輯", }, Rename: "重命名對話", Typing: "正在輸入…", diff --git a/app/requests.ts b/app/requests.ts index d173eb0de..caab4edf9 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -174,6 +174,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) { role: "user", content: prompt, date: new Date().toLocaleString(), + isEditing: false }, ]); diff --git a/app/store/app.ts b/app/store/app.ts index 6ab3229ac..98982ca1e 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -14,6 +14,7 @@ import Locale from "../locales"; export type Message = ChatCompletionResponseMessage & { date: string; streaming?: boolean; + isEditing: boolean; }; export enum SubmitKey { @@ -169,6 +170,7 @@ function createEmptySession(): ChatSession { role: "assistant", content: Locale.Store.BotHello, date: createDate, + isEditing: false }, ], stat: { @@ -190,6 +192,9 @@ interface ChatStore { newSession: () => void; currentSession: () => ChatSession; onNewMessage: (message: Message) => void; + onUserEdit: (message: Message) => void; + onConfirmEdit: (index: number, content: string) => void; + onCancelEdit: (message: Message) => void; onUserInput: (content: string) => Promise; summarizeSession: () => void; updateStat: (message: Message) => void; @@ -297,11 +302,26 @@ export const useChatStore = create()( get().summarizeSession(); }, + onUserEdit(message) { + message.isEditing = true; + set(() => ({})) + }, + + onConfirmEdit(index, content) { + const session = get().currentSession(); + session.messages = session.messages.slice(0, index) + get().onUserInput(content) + }, + onCancelEdit(message) { + message.isEditing = false; + set(() => ({})) + }, async onUserInput(content) { const userMessage: Message = { role: "user", content, date: new Date().toLocaleString(), + isEditing: false }; const botMessage: Message = { @@ -309,6 +329,7 @@ export const useChatStore = create()( role: "assistant", date: new Date().toLocaleString(), streaming: true, + isEditing: false }; // get recent messages @@ -444,6 +465,7 @@ export const useChatStore = create()( role: "system", content: Locale.Store.Prompt.Summarize, date: "", + isEditing: false }), { filterBot: false,