cherry-studio/src/renderer/src/pages/home/Messages/MessageHeader.tsx
亢奋猫 367c4fe6b6
refactor(ui): improve settings tab and assistant item UI (#11819)
* refactor(ui): improve settings tab and assistant item UI

- Remove SettingsTab from HomeTabs, open settings via navbar drawer instead
- Add menu icon to assistant/agent items for quick access to settings popup
- Remove SessionSettingsTab component (consolidated into settings popup)
- Restore avatar display in bubble message style
- Update topic/session item styles for consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(ui): simplify MessageHeader component logic

- Removed unnecessary header visibility check in MessageHeader.
- Updated justifyContent logic for UserWrap to account for multi-select mode.

This change enhances the clarity and maintainability of the MessageHeader component.

* refactor(ui): streamline ChatNavbar and SettingsTab components

- Removed unused chat state from ChatNavbar.
- Updated SettingsTab to conditionally render settings based on active topic or session.
- Enhanced clarity and maintainability by reducing unnecessary checks and improving component logic.

This change improves the overall user experience and code readability.

* refactor(ui): enhance AgentItem and ChatNavbar components for improved UI

- Updated AgentItem to conditionally hide the assistant icon based on settings.
- Enhanced ChatNavbar to display the assistant's emoji and name with a new layout.
- Introduced memoization for assistant name to optimize rendering.

These changes improve the user interface and maintainability of the components.

* refactor(ui): update HtmlArtifactsPopup to start in fullscreen mode

- Changed initial state of isFullscreen in HtmlArtifactsPopup from false to true.

This adjustment enhances the user experience by providing a more immersive view upon opening the popup.

* refactor(types): remove 'settings' tab from Tab type

- Updated the Tab type in chat.ts to remove the 'settings' option, simplifying the available tabs for the chat interface.

This change streamlines the chat functionality and improves code clarity.

* refactor(ui): enhance UserWrap styling in MessageHeader component

- Added flex property to UserWrap to improve layout flexibility.

This change enhances the responsiveness and layout management of the MessageHeader component.

* refactor(ui): update HtmlArtifactsPopup to prevent drag on ViewControls

- Added "nodrag" class to ViewControls to prevent drag events on double click.

This change improves the user interaction by ensuring that double-clicking on the ViewControls does not trigger drag actions.

* refactor(ui): adjust spacing in AgentLabel component

- Updated the gap between items in the AgentLabel component from 1 to 2 for improved layout consistency.

This change enhances the visual spacing and overall user interface of the AgentSettings page.

* refactor(ui): remove unused useSessions hook from AgentItem component

- Eliminated the useSessions hook from the AgentItem component to streamline the code and improve performance.

This change enhances the maintainability of the AgentItem component by removing unnecessary dependencies.

* refactor(ui): optimize MessageHeader component layout and logic

- Introduced a memoized userNameJustifyContent calculation to streamline the justifyContent logic for UserWrap.
- Simplified the HStack component by replacing inline logic with the new memoized value.

These changes enhance the maintainability and clarity of the MessageHeader component.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 16:11:21 +08:00

186 lines
6.2 KiB
TypeScript

import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { HStack } from '@renderer/components/Layout'
import UserPopup from '@renderer/components/Popups/UserPopup'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { getModelLogoById } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import useAvatar from '@renderer/hooks/useAvatar'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelName } from '@renderer/services/ModelService'
import type { Assistant, Model, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { firstLetter, isEmoji, removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Checkbox, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { Sparkle } from 'lucide-react'
import type { FC } from 'react'
import { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
message: Message
assistant: Assistant
model?: Model
topic: Topic
isGroupContextMessage?: boolean
}
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
if (isLocalAi) return AppLogo
return modelId ? getModelLogoById(modelId) : undefined
}
const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic, isGroupContextMessage }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings()
const { chat } = useRuntime()
const { activeTopicOrSession, activeAgentId } = chat
const { agent } = useAgent(activeAgentId)
const isAgentView = activeTopicOrSession === 'session'
const { t } = useTranslation()
const { isBubbleStyle } = useMessageStyle()
const { openMinappById } = useMinappPopup()
const { isMultiSelectMode, selectedMessageIds, handleSelectMessage } = useChatContext(topic)
const isSelected = selectedMessageIds?.includes(message.id)
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, getMessageModelId(message)), [message])
const getUserName = useCallback(() => {
if (isLocalAi && message.role !== 'user') {
return APP_NAME
}
if (isAgentView && message.role === 'assistant') {
return agent?.name ?? t('common.unknown')
}
if (message.role === 'assistant') {
return getModelName(model) || getMessageModelId(message) || ''
}
return userName || t('common.you')
}, [agent?.name, isAgentView, message, model, t, userName])
const isAssistantMessage = message.role === 'assistant'
const isUserMessage = message.role === 'user'
const showMinappIcon = sidebarIcons.visible.includes('minapp')
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && openMinappById(model.provider)
// because don't need openMinappById to be a dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model?.provider, showMinappIcon])
const userNameJustifyContent = useMemo(() => {
if (!isBubbleStyle) return 'flex-start'
if (isUserMessage && !isMultiSelectMode) return 'flex-end'
return 'flex-start'
}, [isBubbleStyle, isUserMessage, isMultiSelectMode])
return (
<Container className="message-header">
{isAssistantMessage ? (
<Avatar
src={avatarSource}
size={35}
style={{
borderRadius: '25%',
cursor: showMinappIcon ? 'pointer' : 'default',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined
}}
onClick={showMiniApp}>
{avatarName}
</Avatar>
) : (
<>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={() => UserPopup.show()} size={35} fontSize={20}>
{avatar}
</EmojiAvatar>
) : (
<Avatar
src={avatar}
size={35}
style={{ borderRadius: '25%', cursor: 'pointer' }}
onClick={() => UserPopup.show()}
/>
)}
</>
)}
<UserWrap>
<HStack alignItems="center" justifyContent={userNameJustifyContent}>
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username}
</UserName>
{isGroupContextMessage && (
<Tooltip title={t('chat.message.useful.tip')}>
<Sparkle fill="var(--color-primary)" strokeWidth={0} size={18} />
</Tooltip>
)}
</HStack>
<InfoWrap className="message-header-info-wrap">
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</InfoWrap>
</UserWrap>
{isMultiSelectMode && (
<Checkbox
checked={isSelected}
onChange={(e) => handleSelectMessage(message.id, e.target.checked)}
style={{ position: 'absolute', right: 0, top: 0 }}
/>
)}
</Container>
)
})
MessageHeader.displayName = 'MessageHeader'
const Container = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
position: relative;
margin-bottom: 10px;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
`
const InfoWrap = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
`
const UserName = styled.span<{ isBubbleStyle?: boolean; theme?: string }>`
font-size: 14px;
font-weight: 600;
color: ${(props) => (props.isBubbleStyle && props.theme === 'dark' ? 'white' : 'var(--color-text)')};
`
const MessageTime = styled.div`
font-size: 10px;
color: var(--color-text-3);
`
export default MessageHeader