mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
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:
parent
8c6684cbdf
commit
1d854c232e
@ -139,7 +139,7 @@ ul {
|
||||
}
|
||||
}
|
||||
.message-content-container {
|
||||
border-radius: 10px 0 10px 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px 16px 10px 16px;
|
||||
background-color: var(--chat-background-user);
|
||||
align-self: self-end;
|
||||
|
||||
@ -55,6 +55,7 @@
|
||||
p {
|
||||
margin: 1em 0;
|
||||
white-space: pre-wrap;
|
||||
line-height: 2em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 5px;
|
||||
@ -108,6 +109,7 @@
|
||||
li code {
|
||||
background: var(--color-background-mute);
|
||||
padding: 3px 5px;
|
||||
margin: 0 2px;
|
||||
border-radius: 5px;
|
||||
word-break: keep-all;
|
||||
white-space: pre;
|
||||
|
||||
@ -1890,7 +1890,6 @@
|
||||
"messages.navigation.none": "None",
|
||||
"messages.prompt": "Show prompt",
|
||||
"messages.title": "Message Settings",
|
||||
"messages.tokens": "Show token usage",
|
||||
"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.",
|
||||
"miniapps": {
|
||||
|
||||
@ -1890,7 +1890,6 @@
|
||||
"messages.navigation.none": "表示しない",
|
||||
"messages.prompt": "プロンプト表示",
|
||||
"messages.title": "メッセージ設定",
|
||||
"messages.tokens": "トークン使用量を表示",
|
||||
"messages.use_serif_font": "セリフフォントを使用",
|
||||
"mineru.api_key": "Mineruでは現在、1日500ページの無料クォータを提供しており、キーを入力する必要はありません。",
|
||||
"miniapps": {
|
||||
|
||||
@ -1890,7 +1890,6 @@
|
||||
"messages.navigation.none": "Не показывать",
|
||||
"messages.prompt": "Показывать подсказки",
|
||||
"messages.title": "Настройки сообщений",
|
||||
"messages.tokens": "Показать использование токенов",
|
||||
"messages.use_serif_font": "Использовать serif шрифт",
|
||||
"mineru.api_key": "Mineru теперь предлагает ежедневную бесплатную квоту в 500 страниц, и вам не нужно вводить ключ.",
|
||||
"miniapps": {
|
||||
|
||||
@ -1890,7 +1890,6 @@
|
||||
"messages.navigation.none": "不显示",
|
||||
"messages.prompt": "显示提示词",
|
||||
"messages.title": "消息设置",
|
||||
"messages.tokens": "显示 Token 用量",
|
||||
"messages.use_serif_font": "使用衬线字体",
|
||||
"mineru.api_key": "MinerU现在提供每日500页的免费额度,您不需要填写密钥。",
|
||||
"miniapps": {
|
||||
|
||||
@ -1890,7 +1890,6 @@
|
||||
"messages.navigation.none": "不顯示",
|
||||
"messages.prompt": "提示詞顯示",
|
||||
"messages.title": "訊息設定",
|
||||
"messages.tokens": "Token 用量顯示",
|
||||
"messages.use_serif_font": "使用襯線字型",
|
||||
"mineru.api_key": "Mineru 現在每天提供 500 頁的免費配額,且無需輸入金鑰。",
|
||||
"miniapps": {
|
||||
|
||||
@ -47,7 +47,7 @@ const MessageItem: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
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 messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { editingMessageId, stopEditing } = useMessageEditing()
|
||||
@ -127,6 +127,8 @@ const MessageItem: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const showHeader = messageStyle === 'plain' || isAssistantMessage
|
||||
|
||||
return (
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
@ -136,14 +138,15 @@ const MessageItem: FC<Props> = ({
|
||||
'message-user': !isAssistantMessage
|
||||
})}
|
||||
ref={messageContainerRef}>
|
||||
<MessageHeader
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
key={getModelUniqId(model)}
|
||||
index={index}
|
||||
topic={topic}
|
||||
/>
|
||||
{showHeader && (
|
||||
<MessageHeader
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
key={getModelUniqId(model)}
|
||||
topic={topic}
|
||||
/>
|
||||
)}
|
||||
{isEditing && (
|
||||
<MessageEditor
|
||||
message={message}
|
||||
@ -167,7 +170,7 @@ const MessageItem: FC<Props> = ({
|
||||
</MessageErrorBoundary>
|
||||
</MessageContentContainer>
|
||||
{showMenubar && (
|
||||
<MessageFooter className="MessageFooter">
|
||||
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage}>
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
@ -224,12 +227,12 @@ const MessageContentContainer = styled(Scrollbar)`
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
const MessageFooter = styled.div`
|
||||
const MessageFooter = styled.div<{ $isLastMessage: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-direction: ${({ $isLastMessage }) => ($isLastMessage ? 'row-reverse' : 'row')};
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-left: 46px;
|
||||
margin-top: 2px;
|
||||
`
|
||||
|
||||
@ -75,14 +75,14 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
|
||||
supportExts,
|
||||
setFiles,
|
||||
undefined, // 不需要setText
|
||||
pasteLongTextAsFile,
|
||||
false, // 不需要 pasteLongTextAsFile
|
||||
pasteLongTextThreshold,
|
||||
undefined, // 不需要text
|
||||
resizeTextArea,
|
||||
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])
|
||||
|
||||
return (
|
||||
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
||||
{editedBlocks
|
||||
.filter((block) => block.type === MessageBlockType.MAIN_TEXT)
|
||||
.map((block) => (
|
||||
<Textarea
|
||||
className={classNames('editing-message', isFileDragging && 'file-dragging')}
|
||||
key={block.id}
|
||||
ref={textareaRef}
|
||||
variant="borderless"
|
||||
value={block.content}
|
||||
onChange={(e) => {
|
||||
handleTextChange(block.id, e.target.value)
|
||||
resizeTextArea()
|
||||
}}
|
||||
onKeyDown={(e) => handleKeyDown(e, block.id)}
|
||||
autoFocus
|
||||
spellCheck={enableSpellCheck}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onFocus={() => {
|
||||
// 记录当前聚焦的组件
|
||||
PasteService.setLastFocusedComponent('messageEditor')
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
// 阻止事件冒泡,避免触发全局的 Electron contextMenu
|
||||
e.stopPropagation()
|
||||
}}
|
||||
style={{
|
||||
fontSize,
|
||||
padding: '0px 15px 8px 15px'
|
||||
}}>
|
||||
<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>
|
||||
<>
|
||||
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
||||
{editedBlocks
|
||||
.filter((block) => block.type === MessageBlockType.MAIN_TEXT)
|
||||
.map((block) => (
|
||||
<Textarea
|
||||
className={classNames('editing-message', isFileDragging && 'file-dragging')}
|
||||
key={block.id}
|
||||
ref={textareaRef}
|
||||
variant="borderless"
|
||||
value={block.content}
|
||||
onChange={(e) => {
|
||||
handleTextChange(block.id, e.target.value)
|
||||
resizeTextArea()
|
||||
}}
|
||||
onKeyDown={(e) => handleKeyDown(e, block.id)}
|
||||
autoFocus
|
||||
spellCheck={enableSpellCheck}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onFocus={() => {
|
||||
// 记录当前聚焦的组件
|
||||
PasteService.setLastFocusedComponent('messageEditor')
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
// 阻止事件冒泡,避免触发全局的 Electron contextMenu
|
||||
e.stopPropagation()
|
||||
}}
|
||||
style={{
|
||||
fontSize,
|
||||
padding: '0px 15px 8px 15px'
|
||||
}}>
|
||||
<TranslateButton onTranslated={onTranslated} />
|
||||
</Textarea>
|
||||
))}
|
||||
</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>
|
||||
<ActionBarLeft>
|
||||
{isUserMessage && (
|
||||
@ -355,17 +356,17 @@ const MessageBlockEditor: FC<Props> = ({ message, topicId, onSave, onResend, onC
|
||||
)}
|
||||
</ActionBarRight>
|
||||
</ActionBar>
|
||||
</EditorContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
padding: 8px 0;
|
||||
padding: 18px 0;
|
||||
padding-bottom: 5px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 15px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 18px;
|
||||
background-color: var(--color-background-opacity);
|
||||
width: 100%;
|
||||
|
||||
|
||||
@ -18,13 +18,10 @@ import { FC, memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageTokens from './MessageTokens'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
model?: Model
|
||||
index: number | undefined
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
@ -33,7 +30,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | 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 { theme } = useTheme()
|
||||
const { userName, sidebarIcons } = useSettings()
|
||||
@ -61,11 +58,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index, topic
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMinappIcon = sidebarIcons.visible.includes('minapp')
|
||||
const { showTokens } = useSettings()
|
||||
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
const isLastMessage = index === 0
|
||||
|
||||
const showMiniApp = useCallback(() => {
|
||||
showMinappIcon && model?.provider && openMinappById(model.provider)
|
||||
@ -110,8 +105,6 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index, topic
|
||||
</UserName>
|
||||
<InfoWrap className="message-header-info-wrap">
|
||||
<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>
|
||||
</UserWrap>
|
||||
{isMultiSelectMode && (
|
||||
@ -149,12 +142,6 @@ const InfoWrap = styled.div`
|
||||
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 }>`
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@ -49,6 +49,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageTokens from './MessageTokens'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
@ -398,172 +400,180 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
|
||||
const softHoverBg = isBubbleStyle && !isLastMessage
|
||||
|
||||
const showMessageTokens = isBubbleStyle ? isAssistantMessage : true
|
||||
|
||||
return (
|
||||
<MenusBar className={classNames({ menubar: true, show: isLastMessage })}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={() => handleResendUserMessage()}
|
||||
$softHoverBg={isBubbleStyle}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{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} />
|
||||
<>
|
||||
{showMessageTokens && <MessageTokens message={message} />}
|
||||
<MenusBar className={classNames({ menubar: true, show: isLastMessage })}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={() => handleResendUserMessage()}
|
||||
$softHoverBg={isBubbleStyle}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
|
||||
<AtSign size={16} />
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{!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')
|
||||
{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>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
|
||||
<AtSign size={16} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!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) {
|
||||
const translationContent = translationBlocks
|
||||
.map((block) => block?.content || '')
|
||||
.join('\n\n')
|
||||
.trim()
|
||||
if (translationBlocks.length > 0) {
|
||||
const translationContent = translationBlocks
|
||||
.map((block) => block?.content || '')
|
||||
.join('\n\n')
|
||||
.trim()
|
||||
|
||||
if (translationContent) {
|
||||
navigator.clipboard.writeText(translationContent)
|
||||
window.message.success({ content: t('translate.copied'), key: 'translate-copy' })
|
||||
} else {
|
||||
window.message.warning({ content: t('translate.empty'), key: 'translate-copy' })
|
||||
if (translationContent) {
|
||||
navigator.clipboard.writeText(translationContent)
|
||||
window.message.success({ content: t('translate.copied'), key: 'translate-copy' })
|
||||
} else {
|
||||
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: () => {
|
||||
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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
onClick: (e) => e.domEvent.stopPropagation()
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="top"
|
||||
arrow>
|
||||
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
$softHoverBg={softHoverBg}>
|
||||
<Languages size={16} />
|
||||
]
|
||||
: [])
|
||||
],
|
||||
onClick: (e) => e.domEvent.stopPropagation()
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="top"
|
||||
arrow>
|
||||
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
$softHoverBg={softHoverBg}>
|
||||
<Languages size={16} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
)}
|
||||
{isAssistantMessage && isGrouped && (
|
||||
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
|
||||
{message.useful ? (
|
||||
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
|
||||
) : (
|
||||
<ThumbsUp size={16} />
|
||||
)}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
)}
|
||||
{isAssistantMessage && isGrouped && (
|
||||
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
|
||||
{message.useful ? (
|
||||
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
|
||||
) : (
|
||||
<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">
|
||||
)}
|
||||
<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}>
|
||||
<Menu size={19} />
|
||||
<Tooltip
|
||||
title={t('common.delete')}
|
||||
mouseEnterDelay={1}
|
||||
open={showDeleteTooltip}
|
||||
onOpenChange={setShowDeleteTooltip}>
|
||||
<Trash size={16} />
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
</MenusBar>
|
||||
</Popconfirm>
|
||||
{!isUserMessage && (
|
||||
<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;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
margin-top: 5px;
|
||||
`
|
||||
|
||||
const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
|
||||
@ -582,8 +593,8 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
transition: all 0.2s ease;
|
||||
&:hover {
|
||||
background-color: ${(props) =>
|
||||
|
||||
@ -11,7 +11,7 @@ interface MessageTokensProps {
|
||||
isLastMessage?: boolean
|
||||
}
|
||||
|
||||
const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
||||
const MessageTokens: React.FC<MessageTokensProps> = ({ message }) => {
|
||||
const { showTokens } = useSettings()
|
||||
// const { generating } = useRuntime()
|
||||
const locateMessage = () => {
|
||||
@ -106,4 +106,4 @@ const MessageMetadata = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default MessgeTokens
|
||||
export default MessageTokens
|
||||
|
||||
@ -167,6 +167,7 @@ const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
margin-top: 3px;
|
||||
`
|
||||
|
||||
const TagsContainer = styled.div`
|
||||
|
||||
@ -41,7 +41,6 @@ import {
|
||||
setRenderInputMessageAsMarkdown,
|
||||
setShowInputEstimatedTokens,
|
||||
setShowPrompt,
|
||||
setShowTokens,
|
||||
setShowTranslateConfirm,
|
||||
setThoughtAutoCollapse
|
||||
} from '@renderer/store/settings'
|
||||
@ -300,11 +299,6 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.tokens')}</SettingRowTitleSmall>
|
||||
<Switch size="small" checked={showTokens} onChange={(checked) => dispatch(setShowTokens(checked))} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
|
||||
@ -450,7 +450,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
className="topics-tab"
|
||||
list={sortedTopics}
|
||||
onUpdate={updateTopics}
|
||||
style={{ padding: '10px 0 10px 10px' }}
|
||||
style={{ padding: '13px 0 10px 10px' }}
|
||||
itemContainerStyle={{ paddingBottom: '8px' }}>
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
|
||||
Loading…
Reference in New Issue
Block a user