feat: messages styles optimization
This commit is contained in:
parent
731fb7860b
commit
8535edbdd1
@ -31,6 +31,7 @@
|
|||||||
--color-icon: #ffffff99;
|
--color-icon: #ffffff99;
|
||||||
--color-icon-white: #ffffff;
|
--color-icon-white: #ffffff;
|
||||||
--color-border: #ffffff20;
|
--color-border: #ffffff20;
|
||||||
|
--color-error: #f44336;
|
||||||
|
|
||||||
--navbar-height: 42px;
|
--navbar-height: 42px;
|
||||||
--sidebar-width: 55px;
|
--sidebar-width: 55px;
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
margin-top: 4px;
|
|
||||||
|
|
||||||
p:first-of-type {
|
p:first-of-type {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|||||||
@ -23,7 +23,8 @@ const resources = {
|
|||||||
duplicate: 'Duplicate',
|
duplicate: 'Duplicate',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
regenerate: 'Regenerate',
|
regenerate: 'Regenerate',
|
||||||
provider: 'Provider'
|
provider: 'Provider',
|
||||||
|
you: 'You'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
@ -168,7 +169,8 @@ const resources = {
|
|||||||
duplicate: '复制',
|
duplicate: '复制',
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
regenerate: '重新生成',
|
regenerate: '重新生成',
|
||||||
provider: '提供商'
|
provider: '提供商',
|
||||||
|
you: '用户'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: '添加',
|
add: '添加',
|
||||||
|
|||||||
@ -170,7 +170,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
{generating && (
|
{generating && (
|
||||||
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
|
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
|
||||||
<ToolbarButton type="text" onClick={onPause}>
|
<ToolbarButton type="text" onClick={onPause}>
|
||||||
<PauseCircleOutlined />
|
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Avatar, Tooltip } from 'antd'
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
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 Markdown from 'react-markdown'
|
||||||
import CodeBlock from './CodeBlock'
|
import CodeBlock from './CodeBlock'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
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 { SyncOutlined } from '@ant-design/icons'
|
||||||
import { firstLetter } from '@renderer/utils'
|
import { firstLetter } from '@renderer/utils'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -25,8 +28,11 @@ interface Props {
|
|||||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const generating = useAppSelector((state) => state.runtime.generating)
|
||||||
|
const { assistant } = useAssistant(message.assistantId)
|
||||||
|
|
||||||
const isLastMessage = index === 0
|
const isLastMessage = index === 0
|
||||||
|
const isUserMessage = message.role === 'user'
|
||||||
const canRegenerate = isLastMessage && message.role === 'assistant'
|
const canRegenerate = isLastMessage && message.role === 'assistant'
|
||||||
|
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
@ -61,17 +67,42 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
return message.content
|
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 (
|
return (
|
||||||
<MessageContainer key={message.id}>
|
<MessageContainer key={message.id} style={{ borderBottom }}>
|
||||||
<AvatarWrapper>
|
<MessageHeader>
|
||||||
{message.role === 'assistant' ? (
|
<AvatarWrapper>
|
||||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
|
{message.role === 'assistant' ? (
|
||||||
{firstLetter(message.modelId).toUpperCase()}
|
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
|
||||||
</Avatar>
|
{firstLetter(message.modelId).toUpperCase()}
|
||||||
) : (
|
</Avatar>
|
||||||
<Avatar src={avatar} />
|
) : (
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
</AvatarWrapper>
|
</MessageHeader>
|
||||||
<MessageContent>
|
<MessageContent>
|
||||||
{message.status === 'sending' && (
|
{message.status === 'sending' && (
|
||||||
<MessageContentLoading>
|
<MessageContentLoading>
|
||||||
@ -84,32 +115,31 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
)}
|
)}
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
<MenusBar className={`menubar ${isLastMessage && 'show'} ${message.content.length < 300 && 'user'}`}>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||||
<EditOutlined onClick={onEdit} />
|
<ActionButton>
|
||||||
|
<EditOutlined onClick={onEdit} />
|
||||||
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||||
<SwitcherOutlined onClick={onCopy} />
|
<ActionButton>
|
||||||
|
<CopyOutlined onClick={onCopy} />
|
||||||
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
||||||
<DeleteOutlined onClick={onDelete} />
|
<ActionButton>
|
||||||
|
<DeleteOutlined onClick={onDelete} />
|
||||||
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canRegenerate && (
|
{canRegenerate && (
|
||||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||||
<SyncOutlined onClick={onRegenerate} />
|
<ActionButton>
|
||||||
|
<SyncOutlined onClick={onRegenerate} />
|
||||||
|
</ActionButton>
|
||||||
</Tooltip>
|
</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>
|
</MenusBar>
|
||||||
)}
|
)}
|
||||||
</MessageContent>
|
</MessageContent>
|
||||||
@ -119,26 +149,21 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
|
|
||||||
const MessageContainer = styled.div`
|
const MessageContainer = styled.div`
|
||||||
display: flex;
|
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;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
padding: 10px 18px;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
.menubar {
|
.menubar {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
&.show {
|
&.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
&.user {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
.menubar {
|
.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`
|
const MessageContentLoading = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -157,17 +223,9 @@ const MessageContentLoading = styled.div`
|
|||||||
const MenusBar = styled.div`
|
const MenusBar = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
gap: 6px;
|
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`
|
const MessageMetadata = styled.div`
|
||||||
@ -176,4 +234,24 @@ const MessageMetadata = styled.div`
|
|||||||
user-select: text;
|
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
|
export default MessageItem
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { NewButton } from '../HomePage'
|
import { NewButton } from '../HomePage'
|
||||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import { capitalizeFirstLetter } from '@renderer/utils'
|
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { upperFirst } from 'lodash'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeAssistant: Assistant
|
activeAssistant: Assistant
|
||||||
@ -47,7 +47,7 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
|
|||||||
<AssistantName>{assistant?.name}</AssistantName>
|
<AssistantName>{assistant?.name}</AssistantName>
|
||||||
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
|
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
|
||||||
<DropdownButton size="small" type="primary" ghost>
|
<DropdownButton size="small" type="primary" ghost>
|
||||||
{model ? capitalizeFirstLetter(model.name) : t('button.select_model')}
|
{model ? upperFirst(model.name) : t('button.select_model')}
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</NavbarCenter>
|
</NavbarCenter>
|
||||||
|
|||||||
@ -199,12 +199,11 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
|
|||||||
return all.usedTokens - 7
|
return all.usedTokens - 7
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首字母大写
|
/**
|
||||||
export const capitalizeFirstLetter = (str: string) => {
|
* is valid proxy url
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
* @param url proxy url
|
||||||
}
|
* @returns boolean
|
||||||
|
*/
|
||||||
// is valid proxy url
|
|
||||||
export const isValidProxyUrl = (url: string) => {
|
export const isValidProxyUrl = (url: string) => {
|
||||||
return url.includes('://')
|
return url.includes('://')
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user