diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index ae80d9ed..eaf15871 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -31,6 +31,7 @@ --color-icon: #ffffff99; --color-icon-white: #ffffff; --color-border: #ffffff20; + --color-error: #f44336; --navbar-height: 42px; --sidebar-width: 55px; diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index ac8f78ab..a6b113d3 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -3,7 +3,6 @@ font-size: 15px; line-height: 1.6; user-select: text; - margin-top: 4px; p:first-of-type { margin-top: 0; diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 74d02857..c48e1fd5 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -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: '添加', diff --git a/src/renderer/src/pages/home/components/Inputbar.tsx b/src/renderer/src/pages/home/components/Inputbar.tsx index eba87f14..4a08c91d 100644 --- a/src/renderer/src/pages/home/components/Inputbar.tsx +++ b/src/renderer/src/pages/home/components/Inputbar.tsx @@ -170,7 +170,7 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { {generating && ( - + )} diff --git a/src/renderer/src/pages/home/components/Message.tsx b/src/renderer/src/pages/home/components/Message.tsx index c3f0d60d..cd575c7a 100644 --- a/src/renderer/src/pages/home/components/Message.tsx +++ b/src/renderer/src/pages/home/components/Message.tsx @@ -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 = ({ 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,17 +67,42 @@ const MessageItem: FC = ({ 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 ( - - - {message.role === 'assistant' ? ( - - {firstLetter(message.modelId).toUpperCase()} - - ) : ( - + + + + {message.role === 'assistant' ? ( + + {firstLetter(message.modelId).toUpperCase()} + + ) : ( + + )} + + {getUserName()} + {dayjs(message.createdAt).format('MM/DD HH:mm')} + + + {message.usage && ( + + Tokens: {message.usage.total_tokens} | ↓{message.usage.prompt_tokens}↑{message.usage.completion_tokens} + )} - + {message.status === 'sending' && ( @@ -84,32 +115,31 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = )} {showMenu && ( - + {message.role === 'user' && ( - + + + )} - + + + - + + + {canRegenerate && ( - + + + )} - {message.modelId} - {message.usage && ( - <> - - tokens: {message.usage.total_tokens} (in:{message.usage.prompt_tokens}/out: - {message.usage.completion_tokens}) - - - )} )} @@ -119,26 +149,21 @@ const MessageItem: FC = ({ 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 diff --git a/src/renderer/src/pages/home/components/NavigationCenter.tsx b/src/renderer/src/pages/home/components/NavigationCenter.tsx index c76ac0d6..9f682f2e 100644 --- a/src/renderer/src/pages/home/components/NavigationCenter.tsx +++ b/src/renderer/src/pages/home/components/NavigationCenter.tsx @@ -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 = ({ activeAssistant }) => { {assistant?.name} - {model ? capitalizeFirstLetter(model.name) : t('button.select_model')} + {model ? upperFirst(model.name) : t('button.select_model')} diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index c98b8e79..4d28e5f9 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -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('://') }