diff --git a/README.md b/README.md index 0205a223..b6df4a57 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai 6. Code highlighting. 7. Mermaid chart -# 👥 Community - -Join our Telegram group to discuss Cherry Studio's latest developments and features! [Telegram community](https://t.me/CherryStudioAI) - # 🖥️ Develop +## Recommended IDE Setup + +- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + ## Project Setup ### Install diff --git a/src/renderer/src/assets/fonts/icon-fonts/iconfont.css b/src/renderer/src/assets/fonts/icon-fonts/iconfont.css index 95abc881..a80dfc13 100644 --- a/src/renderer/src/assets/fonts/icon-fonts/iconfont.css +++ b/src/renderer/src/assets/fonts/icon-fonts/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4563475 */ - src: url('iconfont.woff2?t=1722242729348') format('woff2'), - url('iconfont.woff?t=1722242729348') format('woff'), - url('iconfont.ttf?t=1722242729348') format('truetype'); + src: url('iconfont.woff2?t=1723186111414') format('woff2'), + url('iconfont.woff?t=1723186111414') format('woff'), + url('iconfont.ttf?t=1723186111414') format('truetype'); } .iconfont { @@ -13,6 +13,14 @@ -moz-osx-font-smoothing: grayscale; } +.icon-copy:before { + content: "\e6ae"; +} + +.icon-ic_send:before { + content: "\e795"; +} + .icon-dark1:before { content: "\e72f"; } diff --git a/src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf b/src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf index 5ea01e35..4341a838 100644 Binary files a/src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf and b/src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf differ diff --git a/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff b/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff index d20c02b0..74e47150 100644 Binary files a/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff and b/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff differ diff --git a/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2 b/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2 index fcf48bfc..60ae9bc6 100644 Binary files a/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2 and b/src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2 differ diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index c65e0458..6d677dc1 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -40,6 +40,7 @@ --navbar-background: rgba(0, 0, 0, 0.8); --sidebar-background: rgba(0, 0, 0, 0.8); + --input-bar-background: rgba(255, 255, 255, 0.02); --navbar-height: 42px; --sidebar-width: 55px; @@ -47,7 +48,7 @@ --topic-list-width: 260px; --settings-width: var(--assistants-width); --status-bar-height: 40px; - --input-bar-height: 115px; + --input-bar-height: 85px; } body[theme-mode='light'] { @@ -87,6 +88,7 @@ body[theme-mode='light'] { --navbar-background: rgba(255, 255, 255, 0.8); --sidebar-background: rgba(255, 255, 255, 0.8); + --input-bar-background: rgba(0, 0, 0, 0.02); } *, @@ -155,3 +157,7 @@ body, position: relative; animation: flash 0.5s ease-out infinite alternate; } + +.ant-segmented-group { + gap: 4px; +} diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index 24f40dad..572723b6 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -3,6 +3,7 @@ font-size: 15px; line-height: 1.6; user-select: text; + word-break: break-word; h1:first-child, h2:first-child, @@ -72,6 +73,9 @@ li { margin-bottom: 0.5em; + &::marker { + color: var(--color-text-3); + } } li > ul, @@ -103,7 +107,7 @@ pre { white-space: pre-wrap !important; - padding: 1em; + padding: 1em 0; border-radius: 5px; overflow-x: auto; font-family: 'Fira Code', 'Courier New', Courier, monospace; diff --git a/src/renderer/src/components/Avatar/ModelAvatar.tsx b/src/renderer/src/components/Avatar/ModelAvatar.tsx new file mode 100644 index 00000000..88165d02 --- /dev/null +++ b/src/renderer/src/components/Avatar/ModelAvatar.tsx @@ -0,0 +1,24 @@ +import { getModelLogo } from '@renderer/config/provider' +import { Model } from '@renderer/types' +import { Avatar, AvatarProps } from 'antd' +import { first } from 'lodash' +import { FC } from 'react' + +interface Props { + model: Model + size: number + props?: AvatarProps +} + +const ModelAvatar: FC = ({ model, size, props }) => { + return ( + + {first(model?.name)} + + ) +} + +export default ModelAvatar diff --git a/src/renderer/src/components/TopView/index.tsx b/src/renderer/src/components/TopView/index.tsx index 6d2755e1..868a1c39 100644 --- a/src/renderer/src/components/TopView/index.tsx +++ b/src/renderer/src/components/TopView/index.tsx @@ -39,7 +39,6 @@ const TopViewContainer: React.FC = ({ children }) => { }, [messageApi, modal]) onPop = () => { - console.debug('[TopView] onPop') const views = [...elementsRef.current] views.pop() elementsRef.current = views @@ -47,8 +46,6 @@ const TopViewContainer: React.FC = ({ children }) => { } onShow = ({ element, id }: ElementItem) => { - console.debug('[TopView] onShow', id) - if (!elementsRef.current.find((el) => el.id === id)) { elementsRef.current = elementsRef.current.concat([{ element, id }]) setElements(elementsRef.current) @@ -56,13 +53,11 @@ const TopViewContainer: React.FC = ({ children }) => { } onHide = (id: string) => { - console.debug('[TopView] onHide', id, elementsRef.current) elementsRef.current = elementsRef.current.filter((el) => el.id !== id) setElements(elementsRef.current) } onHideAll = () => { - console.debug('[TopView] onHideAll') setElements([]) elementsRef.current = [] } @@ -76,11 +71,6 @@ const TopViewContainer: React.FC = ({ children }) => { ) }, []) - console.debug( - '[TopView]', - elements.map((el) => [el.id, el.element]) - ) - return ( <> {children} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 95cf273c..2f13444c 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -131,6 +131,7 @@ const resources = { 'messages.use_serif_font': 'Use serif font', 'messages.input.title': 'Input Settings', 'messages.input.show_estimated_tokens': 'Show estimated input tokens', + 'messages.input.send_shortcuts': 'Send shortcuts', 'general.title': 'General Settings', 'general.user_name': 'User Name', 'general.user_name.placeholder': 'Enter your name', @@ -345,6 +346,7 @@ const resources = { 'messages.use_serif_font': '使用衬线字体', 'messages.input.title': '输入设置', 'messages.input.show_estimated_tokens': '状态显示', + 'messages.input.send_shortcuts': '发送快捷键', 'general.title': '常规设置', 'general.user_name': '用户名', 'general.user_name.placeholder': '请输入用户名', diff --git a/src/renderer/src/pages/home/components/Message.tsx b/src/renderer/src/pages/home/components/Message.tsx index 4b57f568..65b636d4 100644 --- a/src/renderer/src/pages/home/components/Message.tsx +++ b/src/renderer/src/pages/home/components/Message.tsx @@ -1,6 +1,5 @@ import { CheckOutlined, - CopyOutlined, DeleteOutlined, EditOutlined, MenuOutlined, @@ -47,6 +46,7 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = const isUserMessage = message.role === 'user' const isAssistantMessage = message.role === 'assistant' const canRegenerate = isLastMessage && isAssistantMessage + const showMetadata = Boolean(message.usage) && !generating const onCopy = useCallback(() => { navigator.clipboard.writeText(message.content) @@ -119,15 +119,15 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = }, [message]) return ( - + {isAssistantMessage ? ( - + {avatarName} ) : ( - + )} {username} @@ -137,55 +137,58 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = - {message.usage && !generating && ( - - Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens} - - )} - {showMenu && ( - - {message.role === 'user' && ( - - - - - - )} - - - {!copied && } - {copied && } - - - {canRegenerate && ( - - - - + + {showMenu && ( + + {message.role === 'user' && ( + + + - - )} - } - onConfirm={() => onDeleteMessage?.(message)}> - - - + )} + + + {!copied && } + {copied && } - - {!isUserMessage && ( - - - - - - )} - - )} + {canRegenerate && ( + + + + + + + + )} + } + onConfirm={() => onDeleteMessage?.(message)}> + + + + + + + {!isUserMessage && ( + + + + + + )} + + )} + {showMetadata && ( + + Tokens: {message?.usage?.total_tokens} | ↑{message?.usage?.prompt_tokens} | ↓ + {message?.usage?.completion_tokens} + + )} + ) @@ -194,9 +197,8 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = const MessageContainer = styled.div` display: flex; flex-direction: column; - padding: 10px 20px; + padding: 0 20px; position: relative; - border-bottom: 0.5px dotted var(--color-border); .menubar { opacity: 0; transition: opacity 0.2s ease; @@ -205,7 +207,7 @@ const MessageContainer = styled.div` } &.user { position: absolute; - top: 15px; + top: 10px; right: 10px; } } @@ -257,6 +259,16 @@ const MessageContent = styled.div` margin-top: 5px; ` +const MessageFooter = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 2px 0; + margin: 2px 0 8px 0; + border-top: 0.5px dashed var(--color-border); +` + const MessageContentLoading = styled.div` display: flex; flex-direction: row; @@ -270,17 +282,18 @@ const MenusBar = styled.div` justify-content: flex-end; align-items: center; gap: 6px; + margin-left: -5px; ` const MessageMetadata = styled.div` font-size: 12px; color: var(--color-text-2); user-select: text; + margin: 2px 0; ` const ActionButton = styled.div` cursor: pointer; - border: 1px solid var(--color-border); border-radius: 8px; display: flex; flex-direction: row; @@ -288,7 +301,15 @@ const ActionButton = styled.div` align-items: center; width: 30px; height: 30px; - .anticon { + transition: all 0.3s ease; + &:hover { + background-color: var(--color-background-mute); + .anticon { + color: var(--color-text-1); + } + } + .anticon, + .iconfont { cursor: pointer; font-size: 14px; color: var(--color-icon); diff --git a/src/renderer/src/pages/home/components/SelectModelButton.tsx b/src/renderer/src/pages/home/components/SelectModelButton.tsx index ba228067..ce7765cc 100644 --- a/src/renderer/src/pages/home/components/SelectModelButton.tsx +++ b/src/renderer/src/pages/home/components/SelectModelButton.tsx @@ -1,7 +1,7 @@ -import { getModelLogo } from '@renderer/config/provider' +import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import { useAssistant } from '@renderer/hooks/useAssistant' import { Assistant } from '@renderer/types' -import { Avatar, Button } from 'antd' +import { Button } from 'antd' import { upperFirst } from 'lodash' import { FC } from 'react' import { useTranslation } from 'react-i18next' @@ -20,7 +20,7 @@ const SelectModelButton: FC = ({ assistant }) => { return ( - + {model ? upperFirst(model.name) : t('button.select_model')} diff --git a/src/renderer/src/pages/home/components/SelectModelDropdown.tsx b/src/renderer/src/pages/home/components/SelectModelDropdown.tsx index 97422c1c..f8de1629 100644 --- a/src/renderer/src/pages/home/components/SelectModelDropdown.tsx +++ b/src/renderer/src/pages/home/components/SelectModelDropdown.tsx @@ -2,7 +2,7 @@ import { getModelLogo } from '@renderer/config/provider' import { useProviders } from '@renderer/hooks/useProvider' import { Model } from '@renderer/types' import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd' -import { first, upperFirst } from 'lodash' +import { first, sortBy, upperFirst } from 'lodash' import { FC, PropsWithChildren } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -22,7 +22,7 @@ const SelectModelDropdown: FC = ({ children, model, o key: p.id, label: p.isSystem ? t(`provider.${p.id}`) : p.name, type: 'group', - children: p.models.map((m) => ({ + children: sortBy(p.models, 'name').map((m) => ({ key: m?.id, label: upperFirst(m?.name), defaultSelectedKeys: [model?.id], diff --git a/src/renderer/src/pages/home/components/input/Inputbar.tsx b/src/renderer/src/pages/home/components/input/Inputbar.tsx index 2541b0ea..986aaa51 100644 --- a/src/renderer/src/pages/home/components/input/Inputbar.tsx +++ b/src/renderer/src/pages/home/components/input/Inputbar.tsx @@ -18,7 +18,7 @@ import store, { useAppSelector } from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' import { Assistant, Message, Topic } from '@renderer/types' import { uuid } from '@renderer/utils' -import { Button, Popconfirm, Tag, Tooltip } from 'antd' +import { Button, Divider, Popconfirm, Tag, Tooltip } from 'antd' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import dayjs from 'dayjs' import { debounce, isEmpty } from 'lodash' @@ -37,6 +37,7 @@ let _text = '' const Inputbar: FC = ({ assistant, setActiveTopic }) => { const [text, setText] = useState(_text) + const [inputFocus, setInputFocus] = useState(false) const { addTopic } = useAssistant(assistant.id) const { sendMessageShortcut, showInputEstimatedTokens } = useSettings() const [expended, setExpend] = useState(false) @@ -141,7 +142,10 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { }, [assistant]) return ( - + @@ -179,11 +183,20 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { {showInputEstimatedTokens && ( - - {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} - - - ↑ {`${inputTokenCount} / ${estimateTokenCount}`} + + + + {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} + ↑ + {`${inputTokenCount} / ${estimateTokenCount}`} + )} @@ -196,7 +209,7 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { )} - +