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;
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;

View File

@ -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<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 [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<HTMLDivElement>(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: {
<div className={styles["chat-body"]}>
{messages.map((message, i) => {
const isUser = message.role === "user";
return (
<div
key={i}
@ -445,6 +467,16 @@ export function Chat(props: {
</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) &&
!isUser ? (
<LoadingIcon />
@ -455,7 +487,19 @@ export function Chat(props: {
onContextMenu={(e) => onRightClick(e, message)}
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>
@ -466,6 +510,16 @@ export function Chat(props: {
</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>
);

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: "复制",
Stop: "停止",
Retry: "重试",
Edit: "编辑"
},
Rename: "重命名对话",
Typing: "正在输入…",

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void>;
summarizeSession: () => void;
updateStat: (message: Message) => void;
@ -297,11 +302,26 @@ export const useChatStore = create<ChatStore>()(
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<ChatStore>()(
role: "assistant",
date: new Date().toLocaleString(),
streaming: true,
isEditing: false
};
// get recent messages
@ -444,6 +465,7 @@ export const useChatStore = create<ChatStore>()(
role: "system",
content: Locale.Store.Prompt.Summarize,
date: "",
isEditing: false
}),
{
filterBot: false,