feat: messages styles optimization

This commit is contained in:
kangfenmao 2024-07-23 14:51:55 +08:00
parent 731fb7860b
commit 8535edbdd1
7 changed files with 139 additions and 60 deletions

View File

@ -31,6 +31,7 @@
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff20;
--color-error: #f44336;
--navbar-height: 42px;
--sidebar-width: 55px;

View File

@ -3,7 +3,6 @@
font-size: 15px;
line-height: 1.6;
user-select: text;
margin-top: 4px;
p:first-of-type {
margin-top: 0;

View File

@ -23,7 +23,8 @@ const resources = {
duplicate: 'Duplicate',
copy: 'Copy',
regenerate: 'Regenerate',
provider: 'Provider'
provider: 'Provider',
you: 'You'
},
button: {
add: 'Add',
@ -168,7 +169,8 @@ const resources = {
duplicate: '复制',
copy: '复制',
regenerate: '重新生成',
provider: '提供商'
provider: '提供商',
you: '用户'
},
button: {
add: '添加',

View File

@ -170,7 +170,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
{generating && (
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause}>
<PauseCircleOutlined />
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
</ToolbarButton>
</Tooltip>
)}

View File

@ -3,7 +3,7 @@ import { Avatar, Tooltip } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import useAvatar from '@renderer/hooks/useAvatar'
import { DeleteOutlined, EditOutlined, SwitcherOutlined } from '@ant-design/icons'
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import CodeBlock from './CodeBlock'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
@ -12,7 +12,10 @@ import Logo from '@renderer/assets/images/logo.png'
import { SyncOutlined } from '@ant-design/icons'
import { firstLetter } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
import { isEmpty } from 'lodash'
import { isEmpty, upperFirst } from 'lodash'
import dayjs from 'dayjs'
import { useAppSelector } from '@renderer/store'
import { useAssistant } from '@renderer/hooks/useAssistant'
interface Props {
message: Message
@ -25,8 +28,11 @@ interface Props {
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
const avatar = useAvatar()
const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const { assistant } = useAssistant(message.assistantId)
const isLastMessage = index === 0
const isUserMessage = message.role === 'user'
const canRegenerate = isLastMessage && message.role === 'assistant'
const onCopy = () => {
@ -61,8 +67,23 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
return message.content
}
const getUserName = () => {
if (message.id === 'assistant') {
return assistant.name
}
if (message.role === 'assistant') {
return upperFirst(message.modelId)
}
return t('common.you')
}
const borderBottom = (isLastMessage && !isUserMessage) || generating ? 'none' : undefined
return (
<MessageContainer key={message.id}>
<MessageContainer key={message.id} style={{ borderBottom }}>
<MessageHeader>
<AvatarWrapper>
{message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
@ -71,7 +92,17 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
) : (
<Avatar src={avatar} />
)}
<UserWrap>
<UserName>{getUserName()}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
{message.usage && (
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata>
)}
</MessageHeader>
<MessageContent>
{message.status === 'sending' && (
<MessageContentLoading>
@ -84,32 +115,31 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</Markdown>
)}
{showMenu && (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
<MenusBar className={`menubar ${isLastMessage && 'show'} ${message.content.length < 300 && 'user'}`}>
{message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}>
<ActionButton>
<EditOutlined onClick={onEdit} />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<SwitcherOutlined onClick={onCopy} />
<ActionButton>
<CopyOutlined onClick={onCopy} />
</ActionButton>
</Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<ActionButton>
<DeleteOutlined onClick={onDelete} />
</ActionButton>
</Tooltip>
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton>
<SyncOutlined onClick={onRegenerate} />
</ActionButton>
</Tooltip>
)}
<MessageMetadata>{message.modelId}</MessageMetadata>
{message.usage && (
<>
<MessageMetadata>
tokens: {message.usage.total_tokens} (in:{message.usage.prompt_tokens}/out:
{message.usage.completion_tokens})
</MessageMetadata>
</>
)}
</MenusBar>
)}
</MessageContent>
@ -119,26 +149,21 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
padding: 10px 18px;
position: relative;
border-bottom: 0.5px solid var(--color-border);
.menubar {
opacity: 0;
transition: opacity 0.2s ease;
&.show {
opacity: 1;
}
&.user {
position: absolute;
top: 15px;
right: 10px;
}
}
&:hover {
.menubar {
@ -147,6 +172,47 @@ const MessageContent = styled.div`
}
`
const MessageHeader = styled.div`
margin-right: 10px;
margin-bottom: 10px;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
justify-content: space-between;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const UserWrap = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const MessageTime = styled.div`
font-size: 12px;
color: var(--color-text-2);
margin-left: 12px;
`
const UserName = styled.div`
font-size: 14px;
font-weight: 600;
margin-left: 12px;
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
`
const MessageContentLoading = styled.div`
display: flex;
flex-direction: row;
@ -157,17 +223,9 @@ const MessageContentLoading = styled.div`
const MenusBar = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
justify-content: flex-end;
align-items: center;
gap: 6px;
.anticon {
cursor: pointer;
margin-right: 8px;
font-size: 15px;
color: var(--color-icon);
&:hover {
color: var(--color-text-1);
}
}
`
const MessageMetadata = styled.div`
@ -176,4 +234,24 @@ const MessageMetadata = styled.div`
user-select: text;
`
const ActionButton = styled.div`
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: 8px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
.anticon {
cursor: pointer;
font-size: 14px;
color: var(--color-icon);
}
&:hover {
color: var(--color-text-1);
}
`
export default MessageItem

View File

@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { capitalizeFirstLetter } from '@renderer/utils'
import { isMac } from '@renderer/config/constant'
import { upperFirst } from 'lodash'
interface Props {
activeAssistant: Assistant
@ -47,7 +47,7 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
<AssistantName>{assistant?.name}</AssistantName>
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
<DropdownButton size="small" type="primary" ghost>
{model ? capitalizeFirstLetter(model.name) : t('button.select_model')}
{model ? upperFirst(model.name) : t('button.select_model')}
</DropdownButton>
</DropdownMenu>
</NavbarCenter>

View File

@ -199,12 +199,11 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
return all.usedTokens - 7
}
// 首字母大写
export const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// is valid proxy url
/**
* is valid proxy url
* @param url proxy url
* @returns boolean
*/
export const isValidProxyUrl = (url: string) => {
return url.includes('://')
}