mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +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 {
|
.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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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;
|
||||||
`
|
`
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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) =>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user