feat: add message card style switch

This commit is contained in:
kangfenmao 2024-11-01 21:50:40 +08:00
parent 2e9c7d0830
commit 41c3895da4
13 changed files with 123 additions and 52 deletions

View File

@ -28,3 +28,12 @@ export function useSettings() {
} }
} }
} }
export function useMessageStyle() {
const { messageStyle } = useSettings()
const isBubbleStyle = messageStyle === 'bubble'
return {
isBubbleStyle
}
}

View File

@ -64,7 +64,10 @@
"upgrade.success.content": "Please restart the application to complete the upgrade", "upgrade.success.content": "Please restart the application to complete the upgrade",
"upgrade.success.button": "Restart", "upgrade.success.button": "Restart",
"topic.added": "New topic added", "topic.added": "New topic added",
"save.success.title": "Saved successfully" "save.success.title": "Saved successfully",
"message.style": "Message Style",
"message.style.bubble": "Bubble",
"message.style.plain": "Plain"
}, },
"chat": { "chat": {
"save": "Save", "save": "Save",

View File

@ -64,7 +64,10 @@
"upgrade.success.content": "重启用以完成升级", "upgrade.success.content": "重启用以完成升级",
"upgrade.success.button": "重启", "upgrade.success.button": "重启",
"topic.added": "话题添加成功", "topic.added": "话题添加成功",
"save.success.title": "保存成功" "save.success.title": "保存成功",
"message.style": "消息样式",
"message.style.bubble": "气泡",
"message.style.plain": "简洁"
}, },
"chat": { "chat": {
"save": "保存", "save": "保存",

View File

@ -64,7 +64,10 @@
"upgrade.success.content": "請重新啟動應用以完成升級", "upgrade.success.content": "請重新啟動應用以完成升級",
"upgrade.success.button": "重新啟動", "upgrade.success.button": "重新啟動",
"topic.added": "新話題已添加", "topic.added": "新話題已添加",
"save.success.title": "保存成功" "save.success.title": "保存成功",
"message.style": "消息樣式",
"message.style.bubble": "氣泡",
"message.style.plain": "簡潔"
}, },
"chat": { "chat": {
"save": "保存", "save": "保存",

View File

@ -19,11 +19,11 @@ interface Props {
const Chat: FC<Props> = (props) => { const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id) const { assistant } = useAssistant(props.assistant.id)
const { topicPosition } = useSettings() const { topicPosition, messageStyle } = useSettings()
const { showTopics } = useShowTopics() const { showTopics } = useShowTopics()
return ( return (
<Container id="chat"> <Container id="chat" className={messageStyle}>
<Main vertical flex={1} justify="space-between"> <Main vertical flex={1} justify="space-between">
<Messages <Messages
key={props.activeTopic.id} key={props.activeTopic.id}
@ -52,7 +52,35 @@ const Container = styled.div`
height: 100%; height: 100%;
flex: 1; flex: 1;
justify-content: space-between; justify-content: space-between;
background-color: var(--chat-background); &.bubble {
background-color: var(--chat-background);
.system-prompt {
background-color: var(--chat-background-assistant);
}
.message-content-container {
margin: 5px 0;
border-radius: 8px;
padding: 10px 15px 0 15px;
}
.message-user {
.markdown,
.anticon,
.iconfont,
.message-tokens {
color: var(--chat-text-user);
}
.message-action-button:hover {
background-color: var(--color-white-soft);
}
}
#inputbar {
border-radius: 0;
margin: 0;
border: none;
border-top: 1px solid var(--color-border-mute);
background: var(--color-background);
}
}
` `
const Main = styled(Flex)` const Main = styled(Flex)`

View File

@ -12,7 +12,7 @@ import { isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore' import { useShowTopics } from '@renderer/hooks/useStore'
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService' import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
@ -58,6 +58,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const containerRef = useRef(null) const containerRef = useRef(null)
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const { searching } = useRuntime() const { searching } = useRuntime()
const { isBubbleStyle } = useMessageStyle()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isVision = useMemo(() => isVisionModel(model), [model]) const isVision = useMemo(() => isVisionModel(model), [model])
@ -299,7 +300,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
autoFocus autoFocus
contextMenu="true" contextMenu="true"
variant="borderless" variant="borderless"
rows={2} rows={isBubbleStyle ? 2 : 1}
ref={textareaRef} ref={textareaRef}
style={{ fontSize }} style={{ fontSize }}
styles={{ textarea: TextareaStyle }} styles={{ textarea: TextareaStyle }}
@ -375,18 +376,19 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
` `
const InputBarContainer = styled.div`
border: 1px solid var(--color-border-soft);
transition: all 0.3s ease;
position: relative;
margin: 0 20px 15px 20px;
border-radius: 10px;
`
const TextareaStyle: CSSProperties = { const TextareaStyle: CSSProperties = {
paddingLeft: 0, paddingLeft: 0,
padding: '10px 15px 8px' padding: '10px 15px 8px'
} }
const InputBarContainer = styled.div`
border-top: 1px solid var(--color-border-mute);
transition: all 0.3s ease;
position: relative;
background: var(--color-background);
`
const Textarea = styled(TextArea)` const Textarea = styled(TextArea)`
padding: 0; padding: 0;
border-radius: 0; border-radius: 0;

View File

@ -2,7 +2,7 @@ import { FONT_FAMILY } from '@renderer/config/constant'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useModel } from '@renderer/hooks/useModel' import { useModel } from '@renderer/hooks/useModel'
import { useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { fetchChatCompletion } from '@renderer/services/ApiService' import { fetchChatCompletion } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { estimateMessageUsage } from '@renderer/services/TokenService' import { estimateMessageUsage } from '@renderer/services/TokenService'
@ -42,11 +42,13 @@ 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(message.modelId) const model = useModel(message.modelId)
const { isBubbleStyle } = useMessageStyle()
const { showMessageDivider, messageFont, fontSize } = useSettings() const { showMessageDivider, messageFont, fontSize } = useSettings()
const messageContainerRef = useRef<HTMLDivElement>(null) const messageContainerRef = useRef<HTMLDivElement>(null)
const isLastMessage = index === 0 const isLastMessage = index === 0
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMenubar = !message.status.includes('ing') const showMenubar = !message.status.includes('ing')
const fontFamily = useMemo(() => { const fontFamily = useMemo(() => {
@ -54,6 +56,11 @@ const MessageItem: FC<Props> = ({
}, [messageFont]) }, [messageFont])
const messageBorder = showMessageDivider ? undefined : 'none' const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = isBubbleStyle
? isAssistantMessage
? 'var(--chat-background-assistant)'
: 'var(--chat-background-user)'
: undefined
const onEditMessage = useCallback( const onEditMessage = useCallback(
(msg: Message) => { (msg: Message) => {
@ -139,17 +146,19 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage 'message-user': !isAssistantMessage
})} })}
ref={messageContainerRef} ref={messageContainerRef}
style={{ alignItems: isAssistantMessage ? 'start' : 'end' }}> style={isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : undefined}>
<MessageHeader message={message} assistant={assistant} model={model} /> <MessageHeader message={message} assistant={assistant} model={model} />
<MessageContentContainer <MessageContentContainer
style={{ className="message-content-container"
fontFamily, style={{ fontFamily, fontSize, background: messageBackground }}>
fontSize,
background: isAssistantMessage ? 'var(--chat-background-assistant)' : 'var(--chat-background-user)'
}}>
<MessageContent message={message} model={model} /> <MessageContent message={message} model={model} />
{showMenubar && ( {showMenubar && (
<MessageFooter style={{ border: messageBorder }}> <MessageFooter
style={{
border: messageBorder,
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined
}}>
<MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar <MessageMenubar
message={message} message={message}
model={model} model={model}
@ -160,7 +169,6 @@ const MessageItem: FC<Props> = ({
onEditMessage={onEditMessage} onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage} onDeleteMessage={onDeleteMessage}
/> />
<MessageTokens message={message} isLastMessage={isLastMessage} />
</MessageFooter> </MessageFooter>
)} )}
</MessageContentContainer> </MessageContentContainer>
@ -177,17 +185,6 @@ const MessageContainer = styled.div`
&.message-highlight { &.message-highlight {
background-color: var(--color-primary-mute); background-color: var(--color-primary-mute);
} }
&.message-user {
.markdown,
.anticon,
.iconfont,
.message-tokens {
color: var(--chat-text-user);
}
.message-action-button:hover {
background-color: var(--color-white-soft);
}
}
.menubar { .menubar {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@ -208,9 +205,8 @@ const MessageContentContainer = styled.div`
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin: 5px 0; margin-left: 46px;
border-radius: 8px; margin-top: 5px;
padding: 10px 15px 0 15px;
` `
const MessageFooter = styled.div` const MessageFooter = styled.div`

View File

@ -4,12 +4,12 @@ import { startMinAppById } from '@renderer/config/minapps'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { Assistant, Message, Model } from '@renderer/types' import { Assistant, Message, Model } from '@renderer/types'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils' import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { FC, useCallback, useMemo } from 'react' import { CSSProperties, FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -24,8 +24,7 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
const { theme } = useTheme() const { theme } = useTheme()
const { userName } = useSettings() const { userName } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const { isBubbleStyle } = useMessageStyle()
const isAssistantMessage = message.role === 'assistant'
const avatarSource = useMemo(() => { const avatarSource = useMemo(() => {
if (isLocalAi) return AppLogo if (isLocalAi) return AppLogo
@ -39,19 +38,23 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
return userName || t('common.you') return userName || t('common.you')
}, [message.role, model?.id, model?.name, t, userName]) }, [message.role, model?.id, model?.name, t, userName])
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) const isAssistantMessage = message.role === 'assistant'
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = () => model?.provider && startMinAppById(model?.provider) const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
const avatarStyle: CSSProperties | undefined = isBubbleStyle
? {
flexDirection: isAssistantMessage ? 'row' : 'row-reverse',
textAlign: isAssistantMessage ? 'left' : 'right'
}
: undefined
return ( return (
<Container> <Container>
<AvatarWrapper <AvatarWrapper style={avatarStyle}>
style={{
flexDirection: isAssistantMessage ? 'row' : 'row-reverse',
textAlign: isAssistantMessage ? 'left' : 'right'
}}>
{isAssistantMessage ? ( {isAssistantMessage ? (
<Avatar <Avatar
src={avatarSource} src={avatarSource}

View File

@ -18,7 +18,7 @@ const Prompt: FC<Props> = ({ assistant }) => {
} }
return ( return (
<Container onClick={() => AssistantSettingsPopup.show({ assistant })}> <Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })}>
<Text>{prompt}</Text> <Text>{prompt}</Text>
</Container> </Container>
) )
@ -26,7 +26,7 @@ const Prompt: FC<Props> = ({ assistant }) => {
const Container = styled.div` const Container = styled.div`
padding: 10px 20px; padding: 10px 20px;
background-color: var(--chat-background-assistant); background-color: var(--color-background-soft);
margin-bottom: 20px; margin-bottom: 20px;
margin: 0 20px 0 20px; margin: 0 20px 0 20px;
border-radius: 6px; border-radius: 6px;

View File

@ -11,6 +11,7 @@ import {
setFontSize, setFontSize,
setMathEngine, setMathEngine,
setMessageFont, setMessageFont,
setMessageStyle,
setPasteLongTextAsFile, setPasteLongTextAsFile,
setRenderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens, setShowInputEstimatedTokens,
@ -35,6 +36,7 @@ const SettingsTab: FC<Props> = (props) => {
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [fontSizeValue, setFontSizeValue] = useState(fontSize) const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const { messageStyle } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -214,6 +216,18 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
<Select
value={messageStyle}
onChange={(value) => dispatch(setMessageStyle(value))}
style={{ width: 100 }}
size="small">
<Select.Option value="plain">{t('message.message.style.plain')}</Select.Option>
<Select.Option value="bubble">{t('message.message.style.bubble')}</Select.Option>
</Select>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<Select <Select

View File

@ -24,7 +24,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 36, version: 37,
blacklist: ['runtime'], blacklist: ['runtime'],
migrate migrate
}, },

View File

@ -618,6 +618,10 @@ const migrateConfig = {
'36': (state: RootState) => { '36': (state: RootState) => {
state.settings.topicPosition = 'left' state.settings.topicPosition = 'left'
return state return state
},
'37': (state: RootState) => {
state.settings.messageStyle = 'plain'
return state
} }
} }

View File

@ -24,6 +24,7 @@ export interface SettingsState {
renderInputMessageAsMarkdown: boolean renderInputMessageAsMarkdown: boolean
codeShowLineNumbers: boolean codeShowLineNumbers: boolean
mathEngine: 'MathJax' | 'KaTeX' mathEngine: 'MathJax' | 'KaTeX'
messageStyle: 'plain' | 'bubble'
// webdav 配置 host, user, pass, path // webdav 配置 host, user, pass, path
webdavHost: string webdavHost: string
webdavUser: string webdavUser: string
@ -52,6 +53,7 @@ const initialState: SettingsState = {
renderInputMessageAsMarkdown: true, renderInputMessageAsMarkdown: true,
codeShowLineNumbers: false, codeShowLineNumbers: false,
mathEngine: 'MathJax', mathEngine: 'MathJax',
messageStyle: 'plain',
webdavHost: '', webdavHost: '',
webdavUser: '', webdavUser: '',
webdavPass: '', webdavPass: '',
@ -140,6 +142,9 @@ const settingsSlice = createSlice({
}, },
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => { setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
state.mathEngine = action.payload state.mathEngine = action.payload
},
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
state.messageStyle = action.payload
} }
} }
}) })
@ -170,7 +175,8 @@ export const {
setWebdavPass, setWebdavPass,
setWebdavPath, setWebdavPath,
setCodeShowLineNumbers, setCodeShowLineNumbers,
setMathEngine setMathEngine,
setMessageStyle
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer