refactor(Messages): update message styling and structure for improved clarity

- Simplified the message header and footer components by removing unnecessary props and logic.
- Adjusted the message container styles for better alignment and spacing.
- Enhanced the message tokens display logic and corrected the component name for consistency.
- Removed unused translation keys related to token usage from multiple language files to streamline localization.
This commit is contained in:
kangfenmao 2025-07-09 21:41:58 +08:00
parent 8c6684cbdf
commit 1d854c232e
15 changed files with 258 additions and 264 deletions

View File

@ -139,7 +139,7 @@ ul {
} }
} }
.message-content-container { .message-content-container {
border-radius: 10px 0 10px 10px; border-radius: 10px;
padding: 10px 16px 10px 16px; padding: 10px 16px 10px 16px;
background-color: var(--chat-background-user); background-color: var(--chat-background-user);
align-self: self-end; align-self: self-end;

View File

@ -55,6 +55,7 @@
p { p {
margin: 1em 0; margin: 1em 0;
white-space: pre-wrap; white-space: pre-wrap;
line-height: 2em;
&:last-child { &:last-child {
margin-bottom: 5px; margin-bottom: 5px;
@ -108,6 +109,7 @@
li code { li code {
background: var(--color-background-mute); background: var(--color-background-mute);
padding: 3px 5px; padding: 3px 5px;
margin: 0 2px;
border-radius: 5px; border-radius: 5px;
word-break: keep-all; word-break: keep-all;
white-space: pre; white-space: pre;

View File

@ -1890,7 +1890,6 @@
"messages.navigation.none": "None", "messages.navigation.none": "None",
"messages.prompt": "Show prompt", "messages.prompt": "Show prompt",
"messages.title": "Message Settings", "messages.title": "Message Settings",
"messages.tokens": "Show token usage",
"messages.use_serif_font": "Use serif font", "messages.use_serif_font": "Use serif font",
"mineru.api_key": "Mineru now offers a daily free quota of 500 pages, and you do not need to enter a key.", "mineru.api_key": "Mineru now offers a daily free quota of 500 pages, and you do not need to enter a key.",
"miniapps": { "miniapps": {

View File

@ -1890,7 +1890,6 @@
"messages.navigation.none": "表示しない", "messages.navigation.none": "表示しない",
"messages.prompt": "プロンプト表示", "messages.prompt": "プロンプト表示",
"messages.title": "メッセージ設定", "messages.title": "メッセージ設定",
"messages.tokens": "トークン使用量を表示",
"messages.use_serif_font": "セリフフォントを使用", "messages.use_serif_font": "セリフフォントを使用",
"mineru.api_key": "Mineruでは現在、1日500ページの無料クォータを提供しており、キーを入力する必要はありません。", "mineru.api_key": "Mineruでは現在、1日500ページの無料クォータを提供しており、キーを入力する必要はありません。",
"miniapps": { "miniapps": {

View File

@ -1890,7 +1890,6 @@
"messages.navigation.none": "Не показывать", "messages.navigation.none": "Не показывать",
"messages.prompt": "Показывать подсказки", "messages.prompt": "Показывать подсказки",
"messages.title": "Настройки сообщений", "messages.title": "Настройки сообщений",
"messages.tokens": "Показать использование токенов",
"messages.use_serif_font": "Использовать serif шрифт", "messages.use_serif_font": "Использовать serif шрифт",
"mineru.api_key": "Mineru теперь предлагает ежедневную бесплатную квоту в 500 страниц, и вам не нужно вводить ключ.", "mineru.api_key": "Mineru теперь предлагает ежедневную бесплатную квоту в 500 страниц, и вам не нужно вводить ключ.",
"miniapps": { "miniapps": {

View File

@ -1890,7 +1890,6 @@
"messages.navigation.none": "不显示", "messages.navigation.none": "不显示",
"messages.prompt": "显示提示词", "messages.prompt": "显示提示词",
"messages.title": "消息设置", "messages.title": "消息设置",
"messages.tokens": "显示 Token 用量",
"messages.use_serif_font": "使用衬线字体", "messages.use_serif_font": "使用衬线字体",
"mineru.api_key": "MinerU现在提供每日500页的免费额度您不需要填写密钥。", "mineru.api_key": "MinerU现在提供每日500页的免费额度您不需要填写密钥。",
"miniapps": { "miniapps": {

View File

@ -1890,7 +1890,6 @@
"messages.navigation.none": "不顯示", "messages.navigation.none": "不顯示",
"messages.prompt": "提示詞顯示", "messages.prompt": "提示詞顯示",
"messages.title": "訊息設定", "messages.title": "訊息設定",
"messages.tokens": "Token 用量顯示",
"messages.use_serif_font": "使用襯線字型", "messages.use_serif_font": "使用襯線字型",
"mineru.api_key": "Mineru 現在每天提供 500 頁的免費配額,且無需輸入金鑰。", "mineru.api_key": "Mineru 現在每天提供 500 頁的免費配額,且無需輸入金鑰。",
"miniapps": { "miniapps": {

View File

@ -47,7 +47,7 @@ const MessageItem: FC<Props> = ({
const { t } = useTranslation() const { t } = useTranslation()
const { assistant, setModel } = useAssistant(message.assistantId) const { assistant, setModel } = useAssistant(message.assistantId)
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
const { messageFont, fontSize } = useSettings() const { messageFont, fontSize, messageStyle } = useSettings()
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic) const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
const messageContainerRef = useRef<HTMLDivElement>(null) const messageContainerRef = useRef<HTMLDivElement>(null)
const { editingMessageId, stopEditing } = useMessageEditing() const { editingMessageId, stopEditing } = useMessageEditing()
@ -127,6 +127,8 @@ const MessageItem: FC<Props> = ({
) )
} }
const showHeader = messageStyle === 'plain' || isAssistantMessage
return ( return (
<MessageContainer <MessageContainer
key={message.id} key={message.id}
@ -136,14 +138,15 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage 'message-user': !isAssistantMessage
})} })}
ref={messageContainerRef}> ref={messageContainerRef}>
<MessageHeader {showHeader && (
message={message} <MessageHeader
assistant={assistant} message={message}
model={model} assistant={assistant}
key={getModelUniqId(model)} model={model}
index={index} key={getModelUniqId(model)}
topic={topic} topic={topic}
/> />
)}
{isEditing && ( {isEditing && (
<MessageEditor <MessageEditor
message={message} message={message}
@ -167,7 +170,7 @@ const MessageItem: FC<Props> = ({
</MessageErrorBoundary> </MessageErrorBoundary>
</MessageContentContainer> </MessageContentContainer>
{showMenubar && ( {showMenubar && (
<MessageFooter className="MessageFooter"> <MessageFooter className="MessageFooter" $isLastMessage={isLastMessage}>
<MessageMenubar <MessageMenubar
message={message} message={message}
assistant={assistant} assistant={assistant}
@ -224,12 +227,12 @@ const MessageContentContainer = styled(Scrollbar)`
overflow-y: auto; overflow-y: auto;
` `
const MessageFooter = styled.div` const MessageFooter = styled.div<{ $isLastMessage: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: ${({ $isLastMessage }) => ($isLastMessage ? 'row-reverse' : 'row')};
justify-content: space-between;
align-items: center; align-items: center;
gap: 20px; justify-content: space-between;
gap: 10px;
margin-left: 46px; margin-left: 46px;
margin-top: 2px; margin-top: 2px;
` `

View File

@ -75,14 +75,14 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
supportExts, supportExts,
setFiles, setFiles,
undefined, // 不需要setText undefined, // 不需要setText
pasteLongTextAsFile, false, // 不需要 pasteLongTextAsFile
pasteLongTextThreshold, pasteLongTextThreshold,
undefined, // 不需要text undefined, // 不需要text
resizeTextArea, resizeTextArea,
t t
) )
}, },
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t] [model, pasteLongTextThreshold, resizeTextArea, supportExts, t]
) )
// 添加全局粘贴事件处理 // 添加全局粘贴事件处理
@ -256,71 +256,72 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
}, [couldAddImageFile, couldAddTextFile]) }, [couldAddImageFile, couldAddTextFile])
return ( return (
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}> <>
{editedBlocks <EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
.filter((block) => block.type === MessageBlockType.MAIN_TEXT) {editedBlocks
.map((block) => ( .filter((block) => block.type === MessageBlockType.MAIN_TEXT)
<Textarea .map((block) => (
className={classNames('editing-message', isFileDragging && 'file-dragging')} <Textarea
key={block.id} className={classNames('editing-message', isFileDragging && 'file-dragging')}
ref={textareaRef} key={block.id}
variant="borderless" ref={textareaRef}
value={block.content} variant="borderless"
onChange={(e) => { value={block.content}
handleTextChange(block.id, e.target.value) onChange={(e) => {
resizeTextArea() handleTextChange(block.id, e.target.value)
}} resizeTextArea()
onKeyDown={(e) => handleKeyDown(e, block.id)} }}
autoFocus onKeyDown={(e) => handleKeyDown(e, block.id)}
spellCheck={enableSpellCheck} autoFocus
onPaste={(e) => onPaste(e.nativeEvent)} spellCheck={enableSpellCheck}
onFocus={() => { onPaste={(e) => onPaste(e.nativeEvent)}
// 记录当前聚焦的组件 onFocus={() => {
PasteService.setLastFocusedComponent('messageEditor') // 记录当前聚焦的组件
}} PasteService.setLastFocusedComponent('messageEditor')
onContextMenu={(e) => { }}
// 阻止事件冒泡,避免触发全局的 Electron contextMenu onContextMenu={(e) => {
e.stopPropagation() // 阻止事件冒泡,避免触发全局的 Electron contextMenu
}} e.stopPropagation()
style={{ }}
fontSize, style={{
padding: '0px 15px 8px 15px' fontSize,
}}> padding: '0px 15px 8px 15px'
<TranslateButton onTranslated={onTranslated} /> }}>
</Textarea> <TranslateButton onTranslated={onTranslated} />
))} </Textarea>
{(editedBlocks.some((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE) ||
files.length > 0) && (
<FileBlocksContainer>
{editedBlocks
.filter((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE)
.map(
(block) =>
block.file && (
<CustomTag
key={block.id}
icon={getFileIcon(block.file.ext)}
color="#37a5aa"
closable
onClose={() => handleFileRemove(block.id)}>
<FileNameRender file={block.file} />
</CustomTag>
)
)}
{files.map((file) => (
<CustomTag
key={file.id}
icon={getFileIcon(file.ext)}
color="#37a5aa"
closable
onClose={() => setFiles((prevFiles) => prevFiles.filter((f) => f.id !== file.id))}>
<FileNameRender file={file} />
</CustomTag>
))} ))}
</FileBlocksContainer> {(editedBlocks.some((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE) ||
)} files.length > 0) && (
<FileBlocksContainer>
{editedBlocks
.filter((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE)
.map(
(block) =>
block.file && (
<CustomTag
key={block.id}
icon={getFileIcon(block.file.ext)}
color="#37a5aa"
closable
onClose={() => handleFileRemove(block.id)}>
<FileNameRender file={block.file} />
</CustomTag>
)
)}
{files.map((file) => (
<CustomTag
key={file.id}
icon={getFileIcon(file.ext)}
color="#37a5aa"
closable
onClose={() => setFiles((prevFiles) => prevFiles.filter((f) => f.id !== file.id))}>
<FileNameRender file={file} />
</CustomTag>
))}
</FileBlocksContainer>
)}
</EditorContainer>
<ActionBar> <ActionBar>
<ActionBarLeft> <ActionBarLeft>
{isUserMessage && ( {isUserMessage && (
@ -355,17 +356,17 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
)} )}
</ActionBarRight> </ActionBarRight>
</ActionBar> </ActionBar>
</EditorContainer> </>
) )
} }
const EditorContainer = styled.div` const EditorContainer = styled.div`
padding: 8px 0; padding: 18px 0;
padding-bottom: 5px;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
transition: all 0.2s ease; transition: all 0.2s ease;
border-radius: 15px; border-radius: 15px;
margin-top: 5px; margin-top: 18px;
margin-bottom: 10px;
background-color: var(--color-background-opacity); background-color: var(--color-background-opacity);
width: 100%; width: 100%;

View File

@ -18,13 +18,10 @@ import { FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
assistant: Assistant assistant: Assistant
model?: Model model?: Model
index: number | undefined
topic: Topic topic: Topic
} }
@ -33,7 +30,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
return modelId ? getModelLogo(modelId) : undefined return modelId ? getModelLogo(modelId) : undefined
} }
const MessageHeader: FC<Props> = memo(({ assistant, model, message, index, topic }) => { const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { theme } = useTheme() const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings() const { userName, sidebarIcons } = useSettings()
@ -61,11 +58,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index, topic
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp') const showMinappIcon = sidebarIcons.visible.includes('minapp')
const { showTokens } = useSettings()
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const isLastMessage = index === 0
const showMiniApp = useCallback(() => { const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && openMinappById(model.provider) showMinappIcon && model?.provider && openMinappById(model.provider)
@ -110,8 +105,6 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index, topic
</UserName> </UserName>
<InfoWrap className="message-header-info-wrap"> <InfoWrap className="message-header-info-wrap">
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime> <MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
{showTokens && <DividerContainer style={{ color: 'var(--color-text-3)' }}> | </DividerContainer>}
<MessageTokens message={message} isLastMessage={isLastMessage} />
</InfoWrap> </InfoWrap>
</UserWrap> </UserWrap>
{isMultiSelectMode && ( {isMultiSelectMode && (
@ -149,12 +142,6 @@ const InfoWrap = styled.div`
gap: 4px; gap: 4px;
` `
const DividerContainer = styled.div`
font-size: 10px;
color: var(--color-text-3);
margin: 0 2px;
`
const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>` const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>`
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;

View File

@ -49,6 +49,8 @@ import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
assistant: Assistant assistant: Assistant
@ -398,172 +400,180 @@ const MessageMenubar: FC<Props> = (props) => {
const softHoverBg = isBubbleStyle && !isLastMessage const softHoverBg = isBubbleStyle && !isLastMessage
const showMessageTokens = isBubbleStyle ? isAssistantMessage : true
return ( return (
<MenusBar className={classNames({ menubar: true, show: isLastMessage })}> <>
{message.role === 'user' && ( {showMessageTokens && <MessageTokens message={message} />}
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}> <MenusBar className={classNames({ menubar: true, show: isLastMessage })}>
<ActionButton {message.role === 'user' && (
className="message-action-button" <Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
onClick={() => handleResendUserMessage()} <ActionButton
$softHoverBg={isBubbleStyle}> className="message-action-button"
<SyncOutlined /> onClick={() => handleResendUserMessage()}
</ActionButton> $softHoverBg={isBubbleStyle}>
</Tooltip> <SyncOutlined />
)}
{message.role === 'user' && (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
<EditOutlined />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
{!copied && <Copy size={16} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton>
</Tooltip>
{isAssistantMessage && (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={onRegenerate}
onOpenChange={(open) => open && setShowRegenerateTooltip(false)}>
<Tooltip
title={t('common.regenerate')}
mouseEnterDelay={0.8}
open={showRegenerateTooltip}
onOpenChange={setShowRegenerateTooltip}>
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
<RefreshCw size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
</Popconfirm> )}
)} {message.role === 'user' && (
{isAssistantMessage && ( <Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}> <ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}> <EditOutlined />
<AtSign size={16} /> </ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
{!copied && <Copy size={16} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} {isAssistantMessage && (
{!isUserMessage && ( <Popconfirm
<Dropdown title={t('message.regenerate.confirm')}
menu={{ okButtonProps={{ danger: true }}
style: { icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
maxHeight: 250, onConfirm={onRegenerate}
overflowY: 'auto', onOpenChange={(open) => open && setShowRegenerateTooltip(false)}>
backgroundClip: 'border-box' <Tooltip
}, title={t('common.regenerate')}
items: [ mouseEnterDelay={0.8}
...translateLanguageOptions.map((item) => ({ open={showRegenerateTooltip}
label: item.emoji + ' ' + item.label(), onOpenChange={setShowRegenerateTooltip}>
key: item.langCode, <ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
onClick: () => handleTranslate(item) <RefreshCw size={16} />
})), </ActionButton>
...(hasTranslationBlocks </Tooltip>
? [ </Popconfirm>
{ type: 'divider' as const }, )}
{ {isAssistantMessage && (
label: '📋 ' + t('common.copy'), <Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
key: 'translate-copy', <ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
onClick: () => { <AtSign size={16} />
const translationBlocks = message.blocks </ActionButton>
.map((blockId) => blockEntities[blockId]) </Tooltip>
.filter((block) => block?.type === 'translation') )}
{!isUserMessage && (
<Dropdown
menu={{
style: {
maxHeight: 250,
overflowY: 'auto',
backgroundClip: 'border-box'
},
items: [
...translateLanguageOptions.map((item) => ({
label: item.emoji + ' ' + item.label(),
key: item.langCode,
onClick: () => handleTranslate(item)
})),
...(hasTranslationBlocks
? [
{ type: 'divider' as const },
{
label: '📋 ' + t('common.copy'),
key: 'translate-copy',
onClick: () => {
const translationBlocks = message.blocks
.map((blockId) => blockEntities[blockId])
.filter((block) => block?.type === 'translation')
if (translationBlocks.length > 0) { if (translationBlocks.length > 0) {
const translationContent = translationBlocks const translationContent = translationBlocks
.map((block) => block?.content || '') .map((block) => block?.content || '')
.join('\n\n') .join('\n\n')
.trim() .trim()
if (translationContent) { if (translationContent) {
navigator.clipboard.writeText(translationContent) navigator.clipboard.writeText(translationContent)
window.message.success({ content: t('translate.copied'), key: 'translate-copy' }) window.message.success({ content: t('translate.copied'), key: 'translate-copy' })
} else { } else {
window.message.warning({ content: t('translate.empty'), key: 'translate-copy' }) window.message.warning({ content: t('translate.empty'), key: 'translate-copy' })
}
}
}
},
{
label: '✖ ' + t('translate.close'),
key: 'translate-close',
onClick: () => {
const translationBlocks = message.blocks
.map((blockId) => blockEntities[blockId])
.filter((block) => block?.type === 'translation')
.map((block) => block?.id)
if (translationBlocks.length > 0) {
translationBlocks.forEach((blockId) => {
if (blockId) removeMessageBlock(message.id, blockId)
})
window.message.success({ content: t('translate.closed'), key: 'translate-close' })
} }
} }
} }
}, ]
{ : [])
label: '✖ ' + t('translate.close'), ],
key: 'translate-close', onClick: (e) => e.domEvent.stopPropagation()
onClick: () => { }}
const translationBlocks = message.blocks trigger={['click']}
.map((blockId) => blockEntities[blockId]) placement="top"
.filter((block) => block?.type === 'translation') arrow>
.map((block) => block?.id) <Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton
if (translationBlocks.length > 0) { className="message-action-button"
translationBlocks.forEach((blockId) => { onClick={(e) => e.stopPropagation()}
if (blockId) removeMessageBlock(message.id, blockId) $softHoverBg={softHoverBg}>
}) <Languages size={16} />
window.message.success({ content: t('translate.closed'), key: 'translate-close' }) </ActionButton>
} </Tooltip>
} </Dropdown>
} )}
] {isAssistantMessage && isGrouped && (
: []) <Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
], <ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
onClick: (e) => e.domEvent.stopPropagation() {message.useful ? (
}} <ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
trigger={['click']} ) : (
placement="top" <ThumbsUp size={16} />
arrow> )}
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Languages size={16} />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
</Dropdown> )}
)} <Popconfirm
{isAssistantMessage && isGrouped && ( title={t('message.message.delete.content')}
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}> okButtonProps={{ danger: true }}
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}> icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
{message.useful ? ( onOpenChange={(open) => open && setShowDeleteTooltip(false)}
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} /> onConfirm={() => deleteMessage(message.id)}>
) : (
<ThumbsUp size={16} />
)}
</ActionButton>
</Tooltip>
)}
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
onConfirm={() => deleteMessage(message.id)}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()} $softHoverBg={softHoverBg}>
<Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
open={showDeleteTooltip}
onOpenChange={setShowDeleteTooltip}>
<Trash size={16} />
</Tooltip>
</ActionButton>
</Popconfirm>
{!isUserMessage && (
<Dropdown
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight">
<ActionButton <ActionButton
className="message-action-button" className="message-action-button"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}> $softHoverBg={softHoverBg}>
<Menu size={19} /> <Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
open={showDeleteTooltip}
onOpenChange={setShowDeleteTooltip}>
<Trash size={16} />
</Tooltip>
</ActionButton> </ActionButton>
</Dropdown> </Popconfirm>
)} {!isUserMessage && (
</MenusBar> <Dropdown
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight">
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Menu size={19} />
</ActionButton>
</Dropdown>
)}
</MenusBar>
</>
) )
} }
@ -572,7 +582,8 @@ const MenusBar = styled.div`
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
margin-top: 5px;
` `
const ActionButton = styled.div<{ $softHoverBg?: boolean }>` const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
@ -582,8 +593,8 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 30px; width: 26px;
height: 30px; height: 26px;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background-color: ${(props) => background-color: ${(props) =>

View File

@ -11,7 +11,7 @@ interface MessageTokensProps {
isLastMessage?: boolean isLastMessage?: boolean
} }
const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => { const MessageTokens: React.FC<MessageTokensProps> = ({ message }) => {
const { showTokens } = useSettings() const { showTokens } = useSettings()
// const { generating } = useRuntime() // const { generating } = useRuntime()
const locateMessage = () => { const locateMessage = () => {
@ -106,4 +106,4 @@ const MessageMetadata = styled.div`
} }
` `
export default MessgeTokens export default MessageTokens

View File

@ -167,6 +167,7 @@ const Container = styled(Scrollbar)`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; padding: 10px;
margin-top: 3px;
` `
const TagsContainer = styled.div` const TagsContainer = styled.div`

View File

@ -41,7 +41,6 @@ import {
setRenderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens, setShowInputEstimatedTokens,
setShowPrompt, setShowPrompt,
setShowTokens,
setShowTranslateConfirm, setShowTranslateConfirm,
setThoughtAutoCollapse setThoughtAutoCollapse
} from '@renderer/store/settings' } from '@renderer/store/settings'
@ -300,11 +299,6 @@ const SettingsTab: FC<Props> = (props) => {
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} /> <Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.tokens')}</SettingRowTitleSmall>
<Switch size="small" checked={showTokens} onChange={(checked) => dispatch(setShowTokens(checked))} />
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch <Switch

View File

@ -450,7 +450,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
className="topics-tab" className="topics-tab"
list={sortedTopics} list={sortedTopics}
onUpdate={updateTopics} onUpdate={updateTopics}
style={{ padding: '10px 0 10px 10px' }} style={{ padding: '13px 0 10px 10px' }}
itemContainerStyle={{ paddingBottom: '8px' }}> itemContainerStyle={{ paddingBottom: '8px' }}>
{(topic) => { {(topic) => {
const isActive = topic.id === activeTopic?.id const isActive = topic.id === activeTopic?.id