feat: new inputbar style

This commit is contained in:
kangfenmao 2024-08-09 18:56:45 +08:00
parent 34d99b711c
commit 4fc53d7c19
19 changed files with 218 additions and 206 deletions

View File

@ -20,12 +20,12 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
6. Code highlighting. 6. Code highlighting.
7. Mermaid chart 7. Mermaid chart
# 👥 Community
Join our Telegram group to discuss Cherry Studio's latest developments and features! [Telegram community](https://t.me/CherryStudioAI)
# 🖥️ Develop # 🖥️ 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 ## Project Setup
### Install ### Install

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4563475 */ font-family: "iconfont"; /* Project id 4563475 */
src: url('iconfont.woff2?t=1722242729348') format('woff2'), src: url('iconfont.woff2?t=1723186111414') format('woff2'),
url('iconfont.woff?t=1722242729348') format('woff'), url('iconfont.woff?t=1723186111414') format('woff'),
url('iconfont.ttf?t=1722242729348') format('truetype'); url('iconfont.ttf?t=1723186111414') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-copy:before {
content: "\e6ae";
}
.icon-ic_send:before {
content: "\e795";
}
.icon-dark1:before { .icon-dark1:before {
content: "\e72f"; content: "\e72f";
} }

View File

@ -40,6 +40,7 @@
--navbar-background: rgba(0, 0, 0, 0.8); --navbar-background: rgba(0, 0, 0, 0.8);
--sidebar-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; --navbar-height: 42px;
--sidebar-width: 55px; --sidebar-width: 55px;
@ -47,7 +48,7 @@
--topic-list-width: 260px; --topic-list-width: 260px;
--settings-width: var(--assistants-width); --settings-width: var(--assistants-width);
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 115px; --input-bar-height: 85px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@ -87,6 +88,7 @@ body[theme-mode='light'] {
--navbar-background: rgba(255, 255, 255, 0.8); --navbar-background: rgba(255, 255, 255, 0.8);
--sidebar-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; position: relative;
animation: flash 0.5s ease-out infinite alternate; animation: flash 0.5s ease-out infinite alternate;
} }
.ant-segmented-group {
gap: 4px;
}

View File

@ -3,6 +3,7 @@
font-size: 15px; font-size: 15px;
line-height: 1.6; line-height: 1.6;
user-select: text; user-select: text;
word-break: break-word;
h1:first-child, h1:first-child,
h2:first-child, h2:first-child,
@ -72,6 +73,9 @@
li { li {
margin-bottom: 0.5em; margin-bottom: 0.5em;
&::marker {
color: var(--color-text-3);
}
} }
li > ul, li > ul,
@ -103,7 +107,7 @@
pre { pre {
white-space: pre-wrap !important; white-space: pre-wrap !important;
padding: 1em; padding: 1em 0;
border-radius: 5px; border-radius: 5px;
overflow-x: auto; overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace; font-family: 'Fira Code', 'Courier New', Courier, monospace;

View File

@ -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<Props> = ({ model, size, props }) => {
return (
<Avatar
src={getModelLogo(model?.id || '')}
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
{...props}>
{first(model?.name)}
</Avatar>
)
}
export default ModelAvatar

View File

@ -39,7 +39,6 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
}, [messageApi, modal]) }, [messageApi, modal])
onPop = () => { onPop = () => {
console.debug('[TopView] onPop')
const views = [...elementsRef.current] const views = [...elementsRef.current]
views.pop() views.pop()
elementsRef.current = views elementsRef.current = views
@ -47,8 +46,6 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
} }
onShow = ({ element, id }: ElementItem) => { onShow = ({ element, id }: ElementItem) => {
console.debug('[TopView] onShow', id)
if (!elementsRef.current.find((el) => el.id === id)) { if (!elementsRef.current.find((el) => el.id === id)) {
elementsRef.current = elementsRef.current.concat([{ element, id }]) elementsRef.current = elementsRef.current.concat([{ element, id }])
setElements(elementsRef.current) setElements(elementsRef.current)
@ -56,13 +53,11 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
} }
onHide = (id: string) => { onHide = (id: string) => {
console.debug('[TopView] onHide', id, elementsRef.current)
elementsRef.current = elementsRef.current.filter((el) => el.id !== id) elementsRef.current = elementsRef.current.filter((el) => el.id !== id)
setElements(elementsRef.current) setElements(elementsRef.current)
} }
onHideAll = () => { onHideAll = () => {
console.debug('[TopView] onHideAll')
setElements([]) setElements([])
elementsRef.current = [] elementsRef.current = []
} }
@ -76,11 +71,6 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
) )
}, []) }, [])
console.debug(
'[TopView]',
elements.map((el) => [el.id, el.element])
)
return ( return (
<> <>
{children} {children}

View File

@ -131,6 +131,7 @@ const resources = {
'messages.use_serif_font': 'Use serif font', 'messages.use_serif_font': 'Use serif font',
'messages.input.title': 'Input Settings', 'messages.input.title': 'Input Settings',
'messages.input.show_estimated_tokens': 'Show estimated input tokens', 'messages.input.show_estimated_tokens': 'Show estimated input tokens',
'messages.input.send_shortcuts': 'Send shortcuts',
'general.title': 'General Settings', 'general.title': 'General Settings',
'general.user_name': 'User Name', 'general.user_name': 'User Name',
'general.user_name.placeholder': 'Enter your name', 'general.user_name.placeholder': 'Enter your name',
@ -345,6 +346,7 @@ const resources = {
'messages.use_serif_font': '使用衬线字体', 'messages.use_serif_font': '使用衬线字体',
'messages.input.title': '输入设置', 'messages.input.title': '输入设置',
'messages.input.show_estimated_tokens': '状态显示', 'messages.input.show_estimated_tokens': '状态显示',
'messages.input.send_shortcuts': '发送快捷键',
'general.title': '常规设置', 'general.title': '常规设置',
'general.user_name': '用户名', 'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名', 'general.user_name.placeholder': '请输入用户名',

View File

@ -1,6 +1,5 @@
import { import {
CheckOutlined, CheckOutlined,
CopyOutlined,
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
MenuOutlined, MenuOutlined,
@ -47,6 +46,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const canRegenerate = isLastMessage && isAssistantMessage const canRegenerate = isLastMessage && isAssistantMessage
const showMetadata = Boolean(message.usage) && !generating
const onCopy = useCallback(() => { const onCopy = useCallback(() => {
navigator.clipboard.writeText(message.content) navigator.clipboard.writeText(message.content)
@ -119,15 +119,15 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
}, [message]) }, [message])
return ( return (
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}> <MessageContainer key={message.id} className="message">
<MessageHeader> <MessageHeader>
<AvatarWrapper> <AvatarWrapper>
{isAssistantMessage ? ( {isAssistantMessage ? (
<Avatar src={avatarSource} size={35}> <Avatar src={avatarSource} size={35} style={{ borderRadius: '20%' }}>
{avatarName} {avatarName}
</Avatar> </Avatar>
) : ( ) : (
<Avatar src={avatar} size={35} /> <Avatar src={avatar} size={35} style={{ borderRadius: '20%' }} />
)} )}
<UserWrap> <UserWrap>
<UserName>{username}</UserName> <UserName>{username}</UserName>
@ -137,11 +137,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</MessageHeader> </MessageHeader>
<MessageContent style={{ fontFamily }}> <MessageContent style={{ fontFamily }}>
<MessageItem /> <MessageItem />
{message.usage && !generating && ( <MessageFooter style={{ border: messageBorder }}>
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata>
)}
{showMenu && ( {showMenu && (
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
@ -153,7 +149,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
)} )}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}> <Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton onClick={onCopy}> <ActionButton onClick={onCopy}>
{!copied && <CopyOutlined />} {!copied && <i className="iconfont icon-copy"></i>}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />} {copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -186,6 +182,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
)} )}
</MenusBar> </MenusBar>
)} )}
{showMetadata && (
<MessageMetadata>
Tokens: {message?.usage?.total_tokens} | {message?.usage?.prompt_tokens} |
{message?.usage?.completion_tokens}
</MessageMetadata>
)}
</MessageFooter>
</MessageContent> </MessageContent>
</MessageContainer> </MessageContainer>
) )
@ -194,9 +197,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 20px; padding: 0 20px;
position: relative; position: relative;
border-bottom: 0.5px dotted var(--color-border);
.menubar { .menubar {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@ -205,7 +207,7 @@ const MessageContainer = styled.div`
} }
&.user { &.user {
position: absolute; position: absolute;
top: 15px; top: 10px;
right: 10px; right: 10px;
} }
} }
@ -257,6 +259,16 @@ const MessageContent = styled.div`
margin-top: 5px; 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` const MessageContentLoading = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -270,17 +282,18 @@ const MenusBar = styled.div`
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
margin-left: -5px;
` `
const MessageMetadata = styled.div` const MessageMetadata = styled.div`
font-size: 12px; font-size: 12px;
color: var(--color-text-2); color: var(--color-text-2);
user-select: text; user-select: text;
margin: 2px 0;
` `
const ActionButton = styled.div` const ActionButton = styled.div`
cursor: pointer; cursor: pointer;
border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -288,7 +301,15 @@ const ActionButton = styled.div`
align-items: center; align-items: center;
width: 30px; width: 30px;
height: 30px; height: 30px;
transition: all 0.3s ease;
&:hover {
background-color: var(--color-background-mute);
.anticon { .anticon {
color: var(--color-text-1);
}
}
.anticon,
.iconfont {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
color: var(--color-icon); color: var(--color-icon);

View File

@ -1,7 +1,7 @@
import { getModelLogo } from '@renderer/config/provider' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { Avatar, Button } from 'antd' import { Button } from 'antd'
import { upperFirst } from 'lodash' import { upperFirst } from 'lodash'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -20,7 +20,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
return ( return (
<SelectModelDropdown model={model} onSelect={setModel}> <SelectModelDropdown model={model} onSelect={setModel}>
<DropdownButton size="small" type="default"> <DropdownButton size="small" type="default">
<Avatar src={getModelLogo(model?.id || '')} style={{ width: 20, height: 20 }} /> <ModelAvatar model={model} size={20} />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName> <ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
</DropdownButton> </DropdownButton>
</SelectModelDropdown> </SelectModelDropdown>

View File

@ -2,7 +2,7 @@ import { getModelLogo } from '@renderer/config/provider'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd' import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
import { first, upperFirst } from 'lodash' import { first, sortBy, upperFirst } from 'lodash'
import { FC, PropsWithChildren } from 'react' import { FC, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -22,7 +22,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
key: p.id, key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name, label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group', type: 'group',
children: p.models.map((m) => ({ children: sortBy(p.models, 'name').map((m) => ({
key: m?.id, key: m?.id,
label: upperFirst(m?.name), label: upperFirst(m?.name),
defaultSelectedKeys: [model?.id], defaultSelectedKeys: [model?.id],

View File

@ -18,7 +18,7 @@ import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' 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 TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { debounce, isEmpty } from 'lodash' import { debounce, isEmpty } from 'lodash'
@ -37,6 +37,7 @@ let _text = ''
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => { const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false)
const { addTopic } = useAssistant(assistant.id) const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings() const { sendMessageShortcut, showInputEstimatedTokens } = useSettings()
const [expended, setExpend] = useState(false) const [expended, setExpend] = useState(false)
@ -141,7 +142,10 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [assistant]) }, [assistant])
return ( return (
<Container id="inputbar" style={{ minHeight: expended ? '80%' : 'var(--input-bar-height)' }}> <Container
id="inputbar"
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
className={inputFocus ? 'focus' : ''}>
<Toolbar> <Toolbar>
<ToolbarMenu> <ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow> <Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
@ -179,11 +183,20 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</Tooltip> </Tooltip>
{showInputEstimatedTokens && ( {showInputEstimatedTokens && (
<TextCount> <TextCount>
<Tooltip title={t('chat.input.context_count.tip')}> <Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag> <Tag
</Tooltip> style={{
<Tooltip title={t('chat.input.estimated_tokens.tip')}> cursor: 'pointer',
<Tag style={{ cursor: 'pointer' }}> {`${inputTokenCount} / ${estimateTokenCount}`}</Tag> borderRadius: '20px',
display: 'flex',
alignItems: 'center',
padding: '2px 8px'
}}>
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />
{`${inputTokenCount} / ${estimateTokenCount}`}
</Tag>
</Tooltip> </Tooltip>
</TextCount> </TextCount>
)} )}
@ -196,7 +209,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} )}
<SendMessageButton sendMessage={sendMessage} /> <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />
</ToolbarMenu> </ToolbarMenu>
</Toolbar> </Toolbar>
<Textarea <Textarea
@ -209,6 +222,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
variant="borderless" variant="borderless"
ref={inputRef} ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }} styles={{ textarea: { paddingLeft: 0 } }}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
/> />
</Container> </Container>
) )
@ -217,10 +232,14 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; height: var(--input-bar-height);
border-top: 0.5px solid var(--color-border); border: 1px solid var(--color-border);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
margin: 0 20px 15px 20px;
border-radius: 10px;
&.focus {
}
` `
const Textarea = styled(TextArea)` const Textarea = styled(TextArea)`
@ -229,13 +248,16 @@ const Textarea = styled(TextArea)`
display: flex; display: flex;
flex: 1; flex: 1;
margin: 0 15px 5px 15px; margin: 0 15px 5px 15px;
font-family: Ubuntu;
resize: vertical;
overflow: auto;
` `
const Toolbar = styled.div` const Toolbar = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: 3px 10px; padding: 0 10px;
` `
const ToolbarMenu = styled.div` const ToolbarMenu = styled.div`

View File

@ -1,44 +1,22 @@
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { SendOutlined } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props { interface Props {
disabled: boolean
sendMessage: () => void sendMessage: () => void
} }
const SendMessageButton: FC<Props> = ({ sendMessage }) => { const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation()
const sendSettingItems: MenuProps['items'] = [
{
label: `Enter ${t('chat.input.send')}`,
key: 'Enter',
icon: <EnterOutlined />,
onClick: () => setSendMessageShortcut('Enter')
},
{
label: `Shift+Enter ${t('chat.input.send')}`,
key: 'Shift+Enter',
icon: <ArrowUpOutlined />,
onClick: () => setSendMessageShortcut('Shift+Enter')
}
]
return ( return (
<Dropdown.Button <i
size="small" className="iconfont icon-ic_send"
onClick={sendMessage} onClick={sendMessage}
trigger={['click']} style={{
arrow cursor: disabled ? 'not-allowed' : 'pointer',
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }} color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
style={{ width: 'auto' }}> fontSize: 22,
{t('chat.input.send')} transition: 'all 0.2s'
<SendOutlined /> }}
</Dropdown.Button> />
) )
} }

View File

@ -1,6 +1,8 @@
import { BarsOutlined, SettingOutlined } from '@ant-design/icons'
import { useShowRightSidebar } from '@renderer/hooks/useStore' import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Segmented } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -54,14 +56,16 @@ const RightSidebar: FC<Props> = (props) => {
return ( return (
<Container> <Container>
<Tabs> <Segmented
<Tab className={tab === 'topic' ? 'active' : ''} onClick={() => setTab('topic')}> value={tab}
{t('common.topics')} style={{ borderRadius: 0, padding: '10px', gap: 5, borderBottom: '0.5px solid var(--color-border)' }}
</Tab> options={[
<Tab className={tab === 'settings' ? 'active' : ''} onClick={() => setTab('settings')}> { label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> },
{t('settings.title')} { label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> }
</Tab> ]}
</Tabs> block
onChange={(value) => setTab(value as 'topic' | 'settings')}
/>
<TabContent> <TabContent>
{tab === 'topic' && <TopicsTab {...props} />} {tab === 'topic' && <TopicsTab {...props} />}
{tab === 'settings' && <SettingsTab assistant={props.assistant} />} {tab === 'settings' && <SettingsTab assistant={props.assistant} />}
@ -82,29 +86,6 @@ const Container = styled.div`
} }
` `
const Tabs = styled.div`
display: flex;
flex-direction: row;
border-bottom: 0.5px solid var(--color-border);
padding: 0 10px;
`
const Tab = styled.div`
padding: 8px 0;
font-weight: 500;
display: flex;
flex: 1;
justify-content: center;
align-items: center;
font-size: 13px;
cursor: pointer;
color: var(--color-text-3);
&.active {
color: var(--color-text-2);
font-weight: 600;
}
`
const TabContent = styled.div` const TabContent = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;

View File

@ -1,4 +1,4 @@
import { QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons' import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
@ -7,7 +7,7 @@ import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@r
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings' import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
import { Assistant, AssistantSettings } from '@renderer/types' import { Assistant, AssistantSettings } from '@renderer/types'
import { Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd' import { Col, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { FC, useCallback, useEffect, useState } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -27,7 +27,8 @@ const SettingsTab: FC<Props> = (props) => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { showMessageDivider, messageFont, showInputEstimatedTokens } = useSettings() const { showMessageDivider, messageFont, showInputEstimatedTokens, sendMessageShortcut, setSendMessageShortcut } =
useSettings()
const onUpdateAssistantSettings = useCallback( const onUpdateAssistantSettings = useCallback(
debounce( debounce(
@ -104,27 +105,15 @@ const SettingsTab: FC<Props> = (props) => {
</Tooltip> </Tooltip>
</Row> </Row>
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={18}> <Col span={24}>
<Slider <Slider
min={0} min={0}
max={1.2} max={1.2}
onChange={onTemperatureChange} onChange={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0} value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1.2: '1.2' }}
step={0.1} step={0.1}
/> />
</Col> </Col>
<Col span={6}>
<InputNumberic
min={0}
max={1.2}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
controls={false}
size="small"
/>
</Col>
</Row> </Row>
<Row align="middle"> <Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label> <Label>{t('chat.settings.conext_count')}</Label>
@ -133,27 +122,15 @@ const SettingsTab: FC<Props> = (props) => {
</Tooltip> </Tooltip>
</Row> </Row>
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={18}> <Col span={24}>
<Slider <Slider
min={0} min={0}
max={20} max={20}
marks={{ 0: '0', 10: '10', 20: t('chat.settings.max') }}
onChange={onConextCountChange} onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0} value={typeof contextCount === 'number' ? contextCount : 0}
step={1} step={1}
/> />
</Col> </Col>
<Col span={6}>
<InputNumberic
min={0}
max={20}
step={1}
value={contextCount}
onChange={onConextCountChange}
controls={false}
size="small"
/>
</Col>
</Row> </Row>
<Row align="middle" justify="space-between" style={{ marginBottom: 8 }}> <Row align="middle" justify="space-between" style={{ marginBottom: 8 }}>
<HStack alignItems="center"> <HStack alignItems="center">
@ -173,7 +150,7 @@ const SettingsTab: FC<Props> = (props) => {
</Row> </Row>
{enableMaxTokens && ( {enableMaxTokens && (
<Row align="middle" gutter={10}> <Row align="middle" gutter={10}>
<Col span={16}> <Col span={24}>
<Slider <Slider
min={0} min={0}
max={32000} max={32000}
@ -182,18 +159,6 @@ const SettingsTab: FC<Props> = (props) => {
step={100} step={100}
/> />
</Col> </Col>
<Col span={8}>
<InputNumberic
min={0}
max={32000}
step={100}
value={maxTokens}
onChange={onMaxTokensChange}
controls={true}
style={{ width: '100%' }}
size="small"
/>
</Col>
</Row> </Row>
)} )}
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle> <SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
@ -227,6 +192,19 @@ const SettingsTab: FC<Props> = (props) => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
</SettingRow>
<Select
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
options={[
{ value: 'Enter', label: `Enter ${t('chat.input.send')}` },
{ value: 'Shift+Enter', label: `Shift + Enter ${t('chat.input.send')}` }
]}
onChange={(value) => setSendMessageShortcut(value)}
style={{ width: '100%', marginTop: 10 }}
/>
</Container> </Container>
) )
} }
@ -238,16 +216,6 @@ const Container = styled.div`
padding: 0 15px; padding: 0 15px;
` `
const InputNumberic = styled(InputNumber)`
width: 45px;
padding: 0;
margin-left: 5px;
text-align: center;
.ant-input-number-input {
text-align: center;
}
`
const Label = styled.p` const Label = styled.p`
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;

View File

@ -136,7 +136,7 @@ const Container = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
padding: 15px 10px; padding: 10px 10px;
` `
const TopicListItem = styled.div` const TopicListItem = styled.div`

View File

@ -8,12 +8,20 @@ import { useTheme } from './ThemeProvider'
const AntdProvider: FC<PropsWithChildren> = ({ children }) => { const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
const { language } = useSettings() const { language } = useSettings()
const { theme: _theme } = useTheme() const { theme: _theme } = useTheme()
const isDarkTheme = _theme === 'dark'
return ( return (
<ConfigProvider <ConfigProvider
locale={getAntdLocale(language)} locale={getAntdLocale(language)}
theme={{ theme={{
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm], algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
components: {
Segmented: {
trackBg: 'transparent',
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
boxShadowTertiary: undefined
}
},
token: { token: {
colorPrimary: '#00b96b', colorPrimary: '#00b96b',
borderRadius: 8 borderRadius: 8