feat: add editable mode
This commit is contained in:
parent
4dbc984351
commit
cc255eea3c
|
@ -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;
|
||||||
|
|
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
{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} />
|
<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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -17,6 +17,7 @@ const cn = {
|
||||||
Copy: "复制",
|
Copy: "复制",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "重试",
|
Retry: "重试",
|
||||||
|
Edit: "编辑"
|
||||||
},
|
},
|
||||||
Rename: "重命名对话",
|
Rename: "重命名对话",
|
||||||
Typing: "正在输入…",
|
Typing: "正在输入…",
|
||||||
|
|
|
@ -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…",
|
||||||
|
|
|
@ -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...",
|
||||||
|
|
|
@ -18,6 +18,7 @@ const tw: LocaleType = {
|
||||||
Copy: "複製",
|
Copy: "複製",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "重試",
|
Retry: "重試",
|
||||||
|
Edit: "編輯",
|
||||||
},
|
},
|
||||||
Rename: "重命名對話",
|
Rename: "重命名對話",
|
||||||
Typing: "正在輸入…",
|
Typing: "正在輸入…",
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue