enhancement: show assistant info on message (#5064)
This commit is contained in:
parent
2d7d731a76
commit
12ad61aaa8
@ -1,6 +1,7 @@
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
import { ThreadContent } from './ThreadContent'
|
||||
import { memo } from 'react'
|
||||
import { useMessages } from '@/hooks/useMessages'
|
||||
|
||||
type Props = {
|
||||
threadId: string
|
||||
@ -9,10 +10,22 @@ type Props = {
|
||||
// Use memo with no dependencies to allow re-renders when props change
|
||||
export const StreamingContent = memo(({ threadId }: Props) => {
|
||||
const { streamingContent } = useAppState()
|
||||
const { getMessages } = useMessages()
|
||||
const messages = getMessages(threadId)
|
||||
|
||||
if (!streamingContent || streamingContent.thread_id !== threadId) return null
|
||||
|
||||
// Pass a new object to ThreadContent to avoid reference issues
|
||||
// The streaming content is always the last message
|
||||
return <ThreadContent {...streamingContent} isLastMessage={true} />
|
||||
return (
|
||||
<ThreadContent
|
||||
{...streamingContent}
|
||||
isLastMessage={true}
|
||||
showAssistant={
|
||||
messages.length > 0
|
||||
? messages[messages.length - 1].role !== 'assistant'
|
||||
: true
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { formatDate } from '@/utils/formatDate'
|
||||
|
||||
const CopyButton = ({ text }: { text: string }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
@ -67,7 +68,13 @@ const CopyButton = ({ text }: { text: string }) => {
|
||||
|
||||
// Use memo to prevent unnecessary re-renders, but allow re-renders when props change
|
||||
export const ThreadContent = memo(
|
||||
(item: ThreadMessage & { isLastMessage?: boolean; index?: number }) => {
|
||||
(
|
||||
item: ThreadMessage & {
|
||||
isLastMessage?: boolean
|
||||
index?: number
|
||||
showAssistant?: boolean
|
||||
}
|
||||
) => {
|
||||
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
|
||||
|
||||
// Use useMemo to stabilize the components prop
|
||||
@ -135,6 +142,10 @@ export const ThreadContent = memo(
|
||||
Array.isArray(item.metadata.tool_calls) &&
|
||||
item.metadata.tool_calls.length
|
||||
|
||||
const assistant = item.metadata?.assistant as
|
||||
| { avatar?: React.ReactNode; name?: React.ReactNode }
|
||||
| undefined
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{item.content?.[0]?.text && item.role === 'user' && (
|
||||
@ -221,6 +232,25 @@ export const ThreadContent = memo(
|
||||
)}
|
||||
{item.content?.[0]?.text && item.role !== 'user' && (
|
||||
<>
|
||||
{item.showAssistant && (
|
||||
<div className="flex items-center gap-2 mb-3 text-main-view-fg/60">
|
||||
<div className="flex items-center gap-2 size-8 rounded-md justify-center border border-main-view-fg/10 bg-main-view-fg/5 p-2">
|
||||
<span className="text-base">{assistant?.avatar || '👋'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<span className="text-main-view-fg font-medium">
|
||||
{assistant?.name || 'Jan'}
|
||||
</span>
|
||||
{item?.created_at && (
|
||||
<span className="text-xs mt-0.5">
|
||||
{formatDate(item?.created_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reasoningSegment && (
|
||||
<ThinkingBlock
|
||||
id={item.index ?? Number(item.id)}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
import { ThreadMessage } from '@janhq/core'
|
||||
import { MCPTool } from '@/types/completion'
|
||||
import { useAssistant } from './useAssistant'
|
||||
|
||||
type AppState = {
|
||||
streamingContent?: ThreadMessage
|
||||
@ -25,8 +26,19 @@ export const useAppState = create<AppState>()((set) => ({
|
||||
serverStatus: 'stopped',
|
||||
abortControllers: {},
|
||||
tokenSpeed: undefined,
|
||||
updateStreamingContent: (content) => {
|
||||
set({ streamingContent: content })
|
||||
updateStreamingContent: (content: ThreadMessage | undefined) => {
|
||||
set(() => ({
|
||||
streamingContent: content
|
||||
? {
|
||||
...content,
|
||||
created_at: content.created_at || Date.now(),
|
||||
metadata: {
|
||||
...content.metadata,
|
||||
assistant: useAssistant.getState().currentAssistant,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}))
|
||||
},
|
||||
updateLoadingModel: (loading) => {
|
||||
set({ loadingModel: loading })
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
createMessage,
|
||||
deleteMessage as deleteMessageExt,
|
||||
} from '@/services/messages'
|
||||
import { useAssistant } from './useAssistant'
|
||||
|
||||
type MessageState = {
|
||||
messages: Record<string, ThreadMessage[]>
|
||||
@ -31,7 +32,16 @@ export const useMessages = create<MessageState>()(
|
||||
}))
|
||||
},
|
||||
addMessage: (message) => {
|
||||
createMessage(message).then((createdMessage) => {
|
||||
const currentAssistant = useAssistant.getState().currentAssistant
|
||||
const newMessage = {
|
||||
...message,
|
||||
created_at: message.created_at || Date.now(),
|
||||
metadata: {
|
||||
...message.metadata,
|
||||
assistant: currentAssistant,
|
||||
},
|
||||
}
|
||||
createMessage(newMessage).then((createdMessage) => {
|
||||
set((state) => ({
|
||||
messages: {
|
||||
...state.messages,
|
||||
|
||||
@ -196,6 +196,11 @@ function ThreadDetail() {
|
||||
<ThreadContent
|
||||
{...item}
|
||||
isLastMessage={isLastMessage}
|
||||
showAssistant={
|
||||
item.role === 'assistant' &&
|
||||
(index === 0 ||
|
||||
messages[index - 1].role !== 'assistant')
|
||||
}
|
||||
index={index}
|
||||
/>
|
||||
</div>
|
||||
|
||||
10
web-app/src/utils/formatDate.ts
Normal file
10
web-app/src/utils/formatDate.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const formatDate = (date: string | number | Date): string => {
|
||||
return new Date(date).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user