enhancement: show assistant info on message (#5064)

This commit is contained in:
Faisal Amir 2025-05-22 14:45:58 +07:00 committed by GitHub
parent 2d7d731a76
commit 12ad61aaa8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 85 additions and 5 deletions

View File

@ -1,6 +1,7 @@
import { useAppState } from '@/hooks/useAppState' import { useAppState } from '@/hooks/useAppState'
import { ThreadContent } from './ThreadContent' import { ThreadContent } from './ThreadContent'
import { memo } from 'react' import { memo } from 'react'
import { useMessages } from '@/hooks/useMessages'
type Props = { type Props = {
threadId: string threadId: string
@ -9,10 +10,22 @@ type Props = {
// Use memo with no dependencies to allow re-renders when props change // Use memo with no dependencies to allow re-renders when props change
export const StreamingContent = memo(({ threadId }: Props) => { export const StreamingContent = memo(({ threadId }: Props) => {
const { streamingContent } = useAppState() const { streamingContent } = useAppState()
const { getMessages } = useMessages()
const messages = getMessages(threadId)
if (!streamingContent || streamingContent.thread_id !== threadId) return null if (!streamingContent || streamingContent.thread_id !== threadId) return null
// Pass a new object to ThreadContent to avoid reference issues // Pass a new object to ThreadContent to avoid reference issues
// The streaming content is always the last message // 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
}
/>
)
}) })

View File

@ -31,6 +31,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { formatDate } from '@/utils/formatDate'
const CopyButton = ({ text }: { text: string }) => { const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false) 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 // Use memo to prevent unnecessary re-renders, but allow re-renders when props change
export const ThreadContent = memo( 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 || '') const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
// Use useMemo to stabilize the components prop // Use useMemo to stabilize the components prop
@ -135,6 +142,10 @@ export const ThreadContent = memo(
Array.isArray(item.metadata.tool_calls) && Array.isArray(item.metadata.tool_calls) &&
item.metadata.tool_calls.length item.metadata.tool_calls.length
const assistant = item.metadata?.assistant as
| { avatar?: React.ReactNode; name?: React.ReactNode }
| undefined
return ( return (
<Fragment> <Fragment>
{item.content?.[0]?.text && item.role === 'user' && ( {item.content?.[0]?.text && item.role === 'user' && (
@ -221,6 +232,25 @@ export const ThreadContent = memo(
)} )}
{item.content?.[0]?.text && item.role !== 'user' && ( {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 && ( {reasoningSegment && (
<ThinkingBlock <ThinkingBlock
id={item.index ?? Number(item.id)} id={item.index ?? Number(item.id)}

View File

@ -1,6 +1,7 @@
import { create } from 'zustand' import { create } from 'zustand'
import { ThreadMessage } from '@janhq/core' import { ThreadMessage } from '@janhq/core'
import { MCPTool } from '@/types/completion' import { MCPTool } from '@/types/completion'
import { useAssistant } from './useAssistant'
type AppState = { type AppState = {
streamingContent?: ThreadMessage streamingContent?: ThreadMessage
@ -25,8 +26,19 @@ export const useAppState = create<AppState>()((set) => ({
serverStatus: 'stopped', serverStatus: 'stopped',
abortControllers: {}, abortControllers: {},
tokenSpeed: undefined, tokenSpeed: undefined,
updateStreamingContent: (content) => { updateStreamingContent: (content: ThreadMessage | undefined) => {
set({ streamingContent: content }) set(() => ({
streamingContent: content
? {
...content,
created_at: content.created_at || Date.now(),
metadata: {
...content.metadata,
assistant: useAssistant.getState().currentAssistant,
},
}
: undefined,
}))
}, },
updateLoadingModel: (loading) => { updateLoadingModel: (loading) => {
set({ loadingModel: loading }) set({ loadingModel: loading })

View File

@ -6,6 +6,7 @@ import {
createMessage, createMessage,
deleteMessage as deleteMessageExt, deleteMessage as deleteMessageExt,
} from '@/services/messages' } from '@/services/messages'
import { useAssistant } from './useAssistant'
type MessageState = { type MessageState = {
messages: Record<string, ThreadMessage[]> messages: Record<string, ThreadMessage[]>
@ -31,7 +32,16 @@ export const useMessages = create<MessageState>()(
})) }))
}, },
addMessage: (message) => { 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) => ({ set((state) => ({
messages: { messages: {
...state.messages, ...state.messages,

View File

@ -196,6 +196,11 @@ function ThreadDetail() {
<ThreadContent <ThreadContent
{...item} {...item}
isLastMessage={isLastMessage} isLastMessage={isLastMessage}
showAssistant={
item.role === 'assistant' &&
(index === 0 ||
messages[index - 1].role !== 'assistant')
}
index={index} index={index}
/> />
</div> </div>

View 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,
})
}