feat: add editable mode

This commit is contained in:
RugerMc 2023-04-01 15:12:11 +08:00
parent 4dbc984351
commit cc255eea3c
10 changed files with 133 additions and 2 deletions

View File

@ -252,6 +252,11 @@
right: 10px; right: 10px;
pointer-events: all; pointer-events: all;
} }
.chat-message-top-left-actions {
opacity: 1;
left:10px;
pointer-events: all;
}
} }
} }
@ -284,6 +289,7 @@
.chat-message-item { .chat-message-item {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;
min-width: 70px;
margin-top: 10px; margin-top: 10px;
border-radius: 10px; border-radius: 10px;
background-color: rgba(0, 0, 0, 0.05); 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 { .chat-message-user > .chat-message-container > .chat-message-item {
background-color: var(--second); background-color: var(--second);
} }
@ -341,6 +374,21 @@
color: #aaa; 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 { .chat-input-panel {
position: absolute; position: absolute;
bottom: 0px; bottom: 0px;

View File

@ -20,6 +20,8 @@ import MenuIcon from "../icons/menu.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.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 { Message, SubmitKey, useChatStore, ChatSession } from "../store";
import { showModal, showToast } from "./ui-lib"; import { showModal, showToast } from "./ui-lib";
@ -190,6 +192,13 @@ export function Chat(props: {
const fontSize = useChatStore((state) => state.config.fontSize); const fontSize = useChatStore((state) => state.config.fontSize);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const MessageInputRefs = useRef<HTMLDivElement[]>([]);
// avoid rendered more hooks error
const setMessageInputRef = (element: HTMLDivElement | null, index: number) => {
if (element) {
MessageInputRefs.current[index] = element;
}
};
const [userInput, setUserInput] = useState(""); const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler(); 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 // for auto-scroll
const latestMessageRef = useRef<HTMLDivElement>(null); const latestMessageRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true); const [autoScroll, setAutoScroll] = useState(true);
@ -302,6 +323,7 @@ export function Chat(props: {
content: "……", content: "……",
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
preview: true, preview: true,
isEditing: false
}, },
] ]
: [], : [],
@ -314,6 +336,7 @@ export function Chat(props: {
content: userInput, content: userInput,
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
preview: true, preview: true,
isEditing: false
}, },
] ]
: [], : [],
@ -400,7 +423,6 @@ export function Chat(props: {
<div className={styles["chat-body"]}> <div className={styles["chat-body"]}>
{messages.map((message, i) => { {messages.map((message, i) => {
const isUser = message.role === "user"; const isUser = message.role === "user";
return ( return (
<div <div
key={i} key={i}
@ -445,6 +467,16 @@ export function Chat(props: {
</div> </div>
</div> </div>
)} )}
{isUser && !message.preview && !message.isEditing && (
<div className={styles["chat-message-top-left-actions"]}>
<div
className={styles["chat-message-top-left-action"]}
onClick={() => onEdit(message)}>,
{Locale.Chat.Actions.Edit}
</div>
</div>
)
}
{(message.preview || message.content.length === 0) && {(message.preview || message.content.length === 0) &&
!isUser ? ( !isUser ? (
<LoadingIcon /> <LoadingIcon />
@ -455,7 +487,19 @@ export function Chat(props: {
onContextMenu={(e) => onRightClick(e, message)} onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => setUserInput(message.content)} onDoubleClickCapture={() => setUserInput(message.content)}
> >
<Markdown content={message.content} /> {message.isEditing ? (
<div
key={i}
ref={(element) => setMessageInputRef(element, i)}
contentEditable={true}
suppressContentEditableWarning={true}
style={{ outline: "0px solid transparent"}}
>
{message.content}
</div>
) : (
<Markdown content={message.content} />
)}
</div> </div>
)} )}
</div> </div>
@ -466,6 +510,16 @@ export function Chat(props: {
</div> </div>
</div> </div>
)} )}
{isUser && message.isEditing && (
<div className={styles["chat-message-actions"]}>
<div className={styles["chat-message-action-edit-button"]} onClick={()=>{confirmEdit(i, MessageInputRefs.current[i].innerText!!)}}>
<OkIcon />
</div>
<div className={styles["chat-message-action-edit-button"]} onClick={()=>{cancelEdit(message)}}>
<ErrorIcon />
</div>
</div>
)}
</div> </div>
</div> </div>
); );

1
app/icons/error.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10" height="10" viewBox="0 0 10 10" fill="none"><defs><rect id="path_0" x="0" y="0" width="10" height="10" /></defs><g opacity="1" transform="translate(0 0) rotate(0 5 5)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#D43030; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.25 1.25) rotate(0 3.75 3.75)" d="M6.46,7.5L7.5,6.46L4.79,3.75L7.5,1.04L6.46,0L3.75,2.71L1.04,0L0,1.04L2.71,3.75L0,6.46L1.04,7.5L3.75,4.79L6.46,7.5Z " /></g></g></svg>

After

Width:  |  Height:  |  Size: 670 B

1
app/icons/ok.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10" height="10" viewBox="0 0 10 10" fill="none"><defs><rect id="path_0" x="0" y="0" width="10" height="10" /></defs><g opacity="1" transform="translate(0 0) rotate(0 5 5)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#43CF7C; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0.8333333333332575 1.875) rotate(0 4.166666666666667 3.125)" d="M3.13,6.25L8.33,1.04L7.29,0L3.13,4.17L1.04,2.08L0,3.13L3.13,6.25Z " /></g></g></svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@ -17,6 +17,7 @@ const cn = {
Copy: "复制", Copy: "复制",
Stop: "停止", Stop: "停止",
Retry: "重试", Retry: "重试",
Edit: "编辑"
}, },
Rename: "重命名对话", Rename: "重命名对话",
Typing: "正在输入…", Typing: "正在输入…",

View File

@ -19,6 +19,7 @@ const en: LocaleType = {
Copy: "Copy", Copy: "Copy",
Stop: "Stop", Stop: "Stop",
Retry: "Retry", Retry: "Retry",
Edit: "Edit",
}, },
Rename: "Rename Chat", Rename: "Rename Chat",
Typing: "Typing…", Typing: "Typing…",

View File

@ -19,6 +19,7 @@ const es: LocaleType = {
Copy: "Copiar", Copy: "Copiar",
Stop: "Detener", Stop: "Detener",
Retry: "Reintentar", Retry: "Reintentar",
Edit: "Editar",
}, },
Rename: "Renombrar chat", Rename: "Renombrar chat",
Typing: "Escribiendo...", Typing: "Escribiendo...",

View File

@ -18,6 +18,7 @@ const tw: LocaleType = {
Copy: "複製", Copy: "複製",
Stop: "停止", Stop: "停止",
Retry: "重試", Retry: "重試",
Edit: "編輯",
}, },
Rename: "重命名對話", Rename: "重命名對話",
Typing: "正在輸入…", Typing: "正在輸入…",

View File

@ -174,6 +174,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
role: "user", role: "user",
content: prompt, content: prompt,
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
isEditing: false
}, },
]); ]);

View File

@ -14,6 +14,7 @@ import Locale from "../locales";
export type Message = ChatCompletionResponseMessage & { export type Message = ChatCompletionResponseMessage & {
date: string; date: string;
streaming?: boolean; streaming?: boolean;
isEditing: boolean;
}; };
export enum SubmitKey { export enum SubmitKey {
@ -169,6 +170,7 @@ function createEmptySession(): ChatSession {
role: "assistant", role: "assistant",
content: Locale.Store.BotHello, content: Locale.Store.BotHello,
date: createDate, date: createDate,
isEditing: false
}, },
], ],
stat: { stat: {
@ -190,6 +192,9 @@ interface ChatStore {
newSession: () => void; newSession: () => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
onNewMessage: (message: Message) => void; onNewMessage: (message: Message) => void;
onUserEdit: (message: Message) => void;
onConfirmEdit: (index: number, content: string) => void;
onCancelEdit: (message: Message) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void; summarizeSession: () => void;
updateStat: (message: Message) => void; updateStat: (message: Message) => void;
@ -297,11 +302,26 @@ export const useChatStore = create<ChatStore>()(
get().summarizeSession(); 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) { async onUserInput(content) {
const userMessage: Message = { const userMessage: Message = {
role: "user", role: "user",
content, content,
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
isEditing: false
}; };
const botMessage: Message = { const botMessage: Message = {
@ -309,6 +329,7 @@ export const useChatStore = create<ChatStore>()(
role: "assistant", role: "assistant",
date: new Date().toLocaleString(), date: new Date().toLocaleString(),
streaming: true, streaming: true,
isEditing: false
}; };
// get recent messages // get recent messages
@ -444,6 +465,7 @@ export const useChatStore = create<ChatStore>()(
role: "system", role: "system",
content: Locale.Store.Prompt.Summarize, content: Locale.Store.Prompt.Summarize,
date: "", date: "",
isEditing: false
}), }),
{ {
filterBot: false, filterBot: false,