diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index ecf45f1b6..3ccf337e8 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -3,6 +3,7 @@ import { atom } from 'jotai' import { conversationStatesAtom, + currentConversationAtom, getActiveConvoIdAtom, updateThreadStateLastMessageAtom, } from './Conversation.atom' @@ -102,6 +103,17 @@ export const cleanConversationMessages = atom(null, (get, set, id: string) => { set(chatMessages, newData) }) +export const deleteMessage = atom(null, (get, set, id: string) => { + const newData: Record = { + ...get(chatMessages), + } + const threadId = get(currentConversationAtom)?.id + if (threadId) { + newData[threadId] = newData[threadId].filter((e) => e.id !== id) + set(chatMessages, newData) + } +}) + export const updateMessageAtom = atom( null, ( diff --git a/web/screens/Chat/MessageToolbar/index.tsx b/web/screens/Chat/MessageToolbar/index.tsx new file mode 100644 index 000000000..60be3860a --- /dev/null +++ b/web/screens/Chat/MessageToolbar/index.tsx @@ -0,0 +1,85 @@ +import { + ChatCompletionRole, + EventName, + MessageStatus, + PluginType, + ThreadMessage, + events, +} from '@janhq/core' +import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins' +import { useAtomValue, useSetAtom } from 'jotai' +import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react' + +import { toaster } from '@/containers/Toast' + +import { + deleteMessage, + getCurrentChatMessagesAtom, +} from '@/helpers/atoms/ChatMessage.atom' +import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom' +import { pluginManager } from '@/plugin' + +const MessageToolbar = ({ message }: { message: ThreadMessage }) => { + const deleteAMessage = useSetAtom(deleteMessage) + const thread = useAtomValue(currentConversationAtom) + const messages = useAtomValue(getCurrentChatMessagesAtom) + const stopInference = async () => { + await pluginManager + .get(PluginType.Inference) + ?.stopInference() + setTimeout(() => { + events.emit(EventName.OnMessageResponseFinished, message) + }, 300) + } + return ( +
+ {message.status === MessageStatus.Pending && ( + stopInference()} + /> + )} + {message.status !== MessageStatus.Pending && + message.id === messages[0]?.id && ( + { + const messageRequest = messages[1] + if (message.role === ChatCompletionRole.Assistant) { + deleteAMessage(message.id ?? '') + } + events.emit(EventName.OnNewMessageRequest, messageRequest) + }} + /> + )} + { + navigator.clipboard.writeText(message.content ?? '') + toaster({ + title: 'Copied to clipboard', + }) + }} + /> + { + deleteAMessage(message.id ?? '') + if (thread) + await pluginManager + .get(PluginType.Conversational) + ?.saveConversation({ + ...thread, + messages: messages.filter((e) => e.id !== message.id), + }) + }} + /> +
+ ) +} + +export default MessageToolbar diff --git a/web/screens/Chat/SimpleTextMessage/index.tsx b/web/screens/Chat/SimpleTextMessage/index.tsx index f67344704..96a408ea0 100644 --- a/web/screens/Chat/SimpleTextMessage/index.tsx +++ b/web/screens/Chat/SimpleTextMessage/index.tsx @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react' +import React, { useContext } from 'react' + +import { ChatCompletionRole, MessageStatus, ThreadMessage } from '@janhq/core' -import { ChatCompletionRole, ThreadMessage } from '@janhq/core' import hljs from 'highlight.js' +import { MoreVertical } from 'lucide-react' import { Marked } from 'marked' import { markedHighlight } from 'marked-highlight' @@ -14,8 +16,12 @@ import LogoMark from '@/containers/Brand/Logo/Mark' import BubbleLoader from '@/containers/Loader/Bubble' +import { FeatureToggleContext } from '@/context/FeatureToggle' + import { displayDate } from '@/utils/datetime' +import MessageToolbar from '../MessageToolbar' + const marked = new Marked( markedHighlight({ langPrefix: 'hljs', @@ -42,12 +48,13 @@ const marked = new Marked( ) const SimpleTextMessage: React.FC = (props) => { + const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) const parsedText = marked.parse(props.content ?? '') const isUser = props.role === ChatCompletionRole.User const isSystem = props.role === ChatCompletionRole.System return ( -
+
= (props) => { {!isUser && !isSystem && }
{props.role}

{displayDate(props.createdAt)}

+ + {experimentalFeatureEnabed && ( +
+ +
+ )}
- {!props.content || props.content === '' ? ( + {props.status === MessageStatus.Pending && + (!props.content || props.content === '') ? ( ) : ( <>