From 7ae6e35746649b37fb974e33e7018ec23faf346a Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 12 Feb 2024 13:00:24 +0700 Subject: [PATCH] feat: add edit messages users (#1974) * feat: add edit message user * fix: delete messages should delete the rest below --------- Co-authored-by: Louis --- web/containers/Providers/Jotai.tsx | 1 + web/helpers/atoms/ChatMessage.atom.ts | 2 + web/hooks/useSendChatMessage.ts | 8 +- web/screens/Chat/EditChatInput/index.tsx | 159 +++++++++++++++++++ web/screens/Chat/MessageToolbar/index.tsx | 30 +++- web/screens/Chat/SimpleTextMessage/index.tsx | 33 ++-- 6 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 web/screens/Chat/EditChatInput/index.tsx diff --git a/web/containers/Providers/Jotai.tsx b/web/containers/Providers/Jotai.tsx index 103f0d9ee..5907ac746 100644 --- a/web/containers/Providers/Jotai.tsx +++ b/web/containers/Providers/Jotai.tsx @@ -8,6 +8,7 @@ type Props = { children: ReactNode } +export const editPromptAtom = atom('') export const currentPromptAtom = atom('') export const fileUploadAtom = atom([]) export const appDownloadProgress = atom(-1) diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index 45cc773e6..904ad344d 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -109,6 +109,8 @@ export const deleteMessageAtom = atom(null, (get, set, id: string) => { } }) +export const editMessageAtom = atom('') + export const updateMessageAtom = atom( null, ( diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index d7c2d10fd..e1c91cca2 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -24,7 +24,11 @@ import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { ulid } from 'ulid' import { selectedModelAtom } from '@/containers/DropdownListSidebar' -import { currentPromptAtom, fileUploadAtom } from '@/containers/Providers/Jotai' +import { + currentPromptAtom, + editPromptAtom, + fileUploadAtom, +} from '@/containers/Providers/Jotai' import { getBase64 } from '@/utils/base64' import { toRuntimeParams, toSettingParams } from '@/utils/modelParam' @@ -54,6 +58,7 @@ export default function useSendChatMessage() { const updateThread = useSetAtom(updateThreadAtom) const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom) const setCurrentPrompt = useSetAtom(currentPromptAtom) + const setEditPrompt = useSetAtom(editPromptAtom) const currentMessages = useAtomValue(getCurrentChatMessagesAtom) const { activeModel } = useActiveModel() @@ -147,6 +152,7 @@ export default function useSendChatMessage() { updateThreadWaiting(activeThread.id, true) const prompt = message.trim() setCurrentPrompt('') + setEditPrompt('') const base64Blob = fileUpload[0] ? await getBase64(fileUpload[0].file).then() diff --git a/web/screens/Chat/EditChatInput/index.tsx b/web/screens/Chat/EditChatInput/index.tsx new file mode 100644 index 000000000..29b81118b --- /dev/null +++ b/web/screens/Chat/EditChatInput/index.tsx @@ -0,0 +1,159 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useRef } from 'react' + +import { + ConversationalExtension, + ExtensionTypeEnum, + InferenceEvent, + MessageStatus, + ThreadMessage, + events, +} from '@janhq/core' + +import { Textarea, Button } from '@janhq/uikit' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' + +import { twMerge } from 'tailwind-merge' + +import { editPromptAtom } from '@/containers/Providers/Jotai' + +import { useActiveModel } from '@/hooks/useActiveModel' + +import useSendChatMessage from '@/hooks/useSendChatMessage' + +import { extensionManager } from '@/extension' + +import { + editMessageAtom, + getCurrentChatMessagesAtom, + setConvoMessagesAtom, +} from '@/helpers/atoms/ChatMessage.atom' +import { + activeThreadAtom, + getActiveThreadIdAtom, + waitingToSendMessage, +} from '@/helpers/atoms/Thread.atom' + +type Props = { + message: ThreadMessage +} + +const EditChatInput: React.FC = ({ message }) => { + const activeThread = useAtomValue(activeThreadAtom) + const { stateModel } = useActiveModel() + const messages = useAtomValue(getCurrentChatMessagesAtom) + + const [editPrompt, setEditPrompt] = useAtom(editPromptAtom) + const { sendChatMessage } = useSendChatMessage() + const setMessages = useSetAtom(setConvoMessagesAtom) + const activeThreadId = useAtomValue(getActiveThreadIdAtom) + + const [isWaitingToSend, setIsWaitingToSend] = useAtom(waitingToSendMessage) + const textareaRef = useRef(null) + const setEditMessage = useSetAtom(editMessageAtom) + + const onPromptChange = (e: React.ChangeEvent) => { + setEditPrompt(e.target.value) + } + + useEffect(() => { + if (isWaitingToSend && activeThreadId) { + setIsWaitingToSend(false) + sendChatMessage(editPrompt) + } + }, [ + activeThreadId, + isWaitingToSend, + editPrompt, + setIsWaitingToSend, + sendChatMessage, + ]) + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.focus() + } + }, [activeThreadId]) + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = '40px' + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px' + } + }, [editPrompt]) + + useEffect(() => { + setEditPrompt(message.content[0]?.text?.value) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const sendEditMessage = async () => { + setEditMessage('') + const messageIdx = messages.findIndex((msg) => msg.id === message.id) + const newMessages = messages.slice(0, messageIdx) + if (activeThread) { + setMessages(activeThread.id, newMessages) + await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.writeMessages( + activeThread.id, + // Remove all of the messages below this + newMessages + ) + .then(() => { + sendChatMessage(editPrompt) + }) + } + } + + const onKeyDown = async (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + if (messages[messages.length - 1]?.status !== MessageStatus.Pending) + sendEditMessage() + else onStopInferenceClick() + } + } + + const onStopInferenceClick = async () => { + events.emit(InferenceEvent.OnInferenceStopped, {}) + } + + return ( +
+
+