feat: message actions tooltip
This commit is contained in:
parent
26bd092f03
commit
5ae319db33
@ -3,6 +3,7 @@ import { atom } from 'jotai'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
conversationStatesAtom,
|
conversationStatesAtom,
|
||||||
|
currentConversationAtom,
|
||||||
getActiveConvoIdAtom,
|
getActiveConvoIdAtom,
|
||||||
updateThreadStateLastMessageAtom,
|
updateThreadStateLastMessageAtom,
|
||||||
} from './Conversation.atom'
|
} from './Conversation.atom'
|
||||||
@ -102,6 +103,17 @@ export const cleanConversationMessages = atom(null, (get, set, id: string) => {
|
|||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deleteMessage = atom(null, (get, set, id: string) => {
|
||||||
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
|
...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(
|
export const updateMessageAtom = atom(
|
||||||
null,
|
null,
|
||||||
(
|
(
|
||||||
|
|||||||
85
web/screens/Chat/MessageToolbar/index.tsx
Normal file
85
web/screens/Chat/MessageToolbar/index.tsx
Normal file
@ -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<InferencePlugin>(PluginType.Inference)
|
||||||
|
?.stopInference()
|
||||||
|
setTimeout(() => {
|
||||||
|
events.emit(EventName.OnMessageResponseFinished, message)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
{message.status === MessageStatus.Pending && (
|
||||||
|
<StopCircle
|
||||||
|
className="mx-1 cursor-pointer rounded-sm bg-gray-800 px-[3px]"
|
||||||
|
size={20}
|
||||||
|
onClick={() => stopInference()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{message.status !== MessageStatus.Pending &&
|
||||||
|
message.id === messages[0]?.id && (
|
||||||
|
<RefreshCcw
|
||||||
|
className="mx-1 cursor-pointer rounded-sm bg-gray-800 px-[3px]"
|
||||||
|
size={20}
|
||||||
|
onClick={() => {
|
||||||
|
const messageRequest = messages[1]
|
||||||
|
if (message.role === ChatCompletionRole.Assistant) {
|
||||||
|
deleteAMessage(message.id ?? '')
|
||||||
|
}
|
||||||
|
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ClipboardCopy
|
||||||
|
className="mx-1 cursor-pointer rounded-sm bg-gray-800 px-[3px]"
|
||||||
|
size={20}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(message.content ?? '')
|
||||||
|
toaster({
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Trash2Icon
|
||||||
|
className="mx-1 cursor-pointer rounded-sm bg-gray-800 px-[3px]"
|
||||||
|
size={20}
|
||||||
|
onClick={async () => {
|
||||||
|
deleteAMessage(message.id ?? '')
|
||||||
|
if (thread)
|
||||||
|
await pluginManager
|
||||||
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
|
?.saveConversation({
|
||||||
|
...thread,
|
||||||
|
messages: messages.filter((e) => e.id !== message.id),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageToolbar
|
||||||
@ -1,9 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* 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 hljs from 'highlight.js'
|
||||||
|
|
||||||
|
import { MoreVertical } from 'lucide-react'
|
||||||
import { Marked } from 'marked'
|
import { Marked } from 'marked'
|
||||||
|
|
||||||
import { markedHighlight } from 'marked-highlight'
|
import { markedHighlight } from 'marked-highlight'
|
||||||
@ -14,8 +16,12 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
|
|||||||
|
|
||||||
import BubbleLoader from '@/containers/Loader/Bubble'
|
import BubbleLoader from '@/containers/Loader/Bubble'
|
||||||
|
|
||||||
|
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||||
|
|
||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
|
import MessageToolbar from '../MessageToolbar'
|
||||||
|
|
||||||
const marked = new Marked(
|
const marked = new Marked(
|
||||||
markedHighlight({
|
markedHighlight({
|
||||||
langPrefix: 'hljs',
|
langPrefix: 'hljs',
|
||||||
@ -42,12 +48,13 @@ const marked = new Marked(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||||
|
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||||
const parsedText = marked.parse(props.content ?? '')
|
const parsedText = marked.parse(props.content ?? '')
|
||||||
const isUser = props.role === ChatCompletionRole.User
|
const isUser = props.role === ChatCompletionRole.User
|
||||||
const isSystem = props.role === ChatCompletionRole.System
|
const isSystem = props.role === ChatCompletionRole.System
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto rounded-xl px-4 lg:w-3/4">
|
<div className="group mx-auto rounded-xl px-4 lg:w-3/4">
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'mb-1 flex items-center justify-start gap-2',
|
'mb-1 flex items-center justify-start gap-2',
|
||||||
@ -57,10 +64,17 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
{!isUser && !isSystem && <LogoMark width={20} />}
|
{!isUser && !isSystem && <LogoMark width={20} />}
|
||||||
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
||||||
<p className="text-xs font-medium">{displayDate(props.createdAt)}</p>
|
<p className="text-xs font-medium">{displayDate(props.createdAt)}</p>
|
||||||
|
|
||||||
|
{experimentalFeatureEnabed && (
|
||||||
|
<div className="hidden cursor-pointer group-hover:flex">
|
||||||
|
<MessageToolbar message={props} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={twMerge('w-full')}>
|
<div className={twMerge('w-full')}>
|
||||||
{!props.content || props.content === '' ? (
|
{props.status === MessageStatus.Pending &&
|
||||||
|
(!props.content || props.content === '') ? (
|
||||||
<BubbleLoader />
|
<BubbleLoader />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user