Compare commits

...

6 Commits

Author SHA1 Message Date
kangfenmao
a22a47c16a chore(version): 0.7.5 2024-09-20 17:01:52 +08:00
kangfenmao
6bb7b2ca5d feat: improved ui effects and rendering for components
- Added smooth all property transition effect to Icon component.
- Added hover effect and conditional rendering for Switch Topic Sidebar button on current assistant.
- Updated the existing conditional options array to consistently include both topic and settings options.
- Improved hover effects on topic list items.
2024-09-20 16:48:24 +08:00
kangfenmao
1ec7df9a7e feat: add new add topic button 2024-09-20 15:11:50 +08:00
kangfenmao
83925832be fix: attachment open handler 2024-09-20 11:38:30 +08:00
kangfenmao
4dadf98909 fix: improved api call validation
- Improved API call validation to account for additional usage properties.
2024-09-20 11:12:15 +08:00
kangfenmao
375c07e442 style: removed unnecessary import and optimized sidebar styling
- Removed unnecessary import and optimized sidebar styling for improved performance.
2024-09-20 10:49:50 +08:00
16 changed files with 227 additions and 105 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.7.4", "version": "0.7.5",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",

View File

@@ -22,7 +22,7 @@
--color-background: #181818; --color-background: #181818;
--color-background-soft: var(--color-black-soft); --color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute); --color-background-mute: var(--color-black-soft);
--color-primary: #00b96b; --color-primary: #00b96b;
--color-primary-soft: #00b96b99; --color-primary-soft: #00b96b99;

View File

@@ -1,3 +1,4 @@
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json' import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
@@ -5,11 +6,13 @@ import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant
import { covertAgentToAssistant } from '@renderer/services/assistant' import { covertAgentToAssistant } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Agent, Assistant } from '@renderer/types' import { Agent, Assistant } from '@renderer/types'
import { Input, Modal, Tag } from 'antd' import { Divider, Input, InputRef, Modal, Tag } from 'antd'
import { useMemo, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { HStack } from '../Layout'
interface Props { interface Props {
resolve: (value: Assistant | undefined) => void resolve: (value: Assistant | undefined) => void
} }
@@ -21,6 +24,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { assistants, addAssistant } = useAssistants() const { assistants, addAssistant } = useAssistants()
const inputRef = useRef<InputRef>(null)
const defaultAgent: Agent = useMemo( const defaultAgent: Agent = useMemo(
() => ({ () => ({
@@ -65,30 +69,52 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
AddAssistantPopup.hide() AddAssistantPopup.hide()
} }
useEffect(() => {
open && setTimeout(() => inputRef.current?.focus(), 0)
}, [open])
return ( return (
<Modal <Modal
centered centered
title={t('chat.add.assistant.title')}
open={open} open={open}
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
transitionName="ant-move-down" transitionName="ant-move-down"
maskTransitionName="ant-fade" maskTransitionName="ant-fade"
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
closeIcon={null}
footer={null}> footer={null}>
<Input <HStack style={{ padding: '0 12px', marginTop: 5 }}>
placeholder={t('common.search')} <Input
value={searchText} prefix={
onChange={(e) => setSearchText(e.target.value)} <SearchIcon>
allowClear <SearchOutlined />
autoFocus </SearchIcon>
style={{ marginBottom: 16 }} }
/> ref={inputRef}
placeholder={t('assistants.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
autoFocus
style={{ paddingLeft: 0 }}
bordered={false}
size="large"
/>
</HStack>
<Divider style={{ margin: 0 }} />
<Container> <Container>
{agents.map((agent) => ( {agents.map((agent) => (
<AgentItem key={agent.id} onClick={() => onCreateAssistant(agent)}> <AgentItem
{agent.emoji} {agent.name} key={agent.id}
{agent.group === 'system' && <Tag color="orange">{t('agents.tag.system')}</Tag>} onClick={() => onCreateAssistant(agent)}
{agent.group === 'user' && <Tag color="green">{t('agents.tag.user')}</Tag>} className={agent.id === 'default' ? 'default' : ''}>
<HStack alignItems="center" gap={5}>
{agent.id === 'default' && <PlusOutlined style={{ marginLeft: -2 }} />}
{agent.emoji} {agent.name}
</HStack>
{agent.group === 'system' && <Tag color="green">{t('agents.tag.system')}</Tag>}
{agent.group === 'user' && <Tag color="orange">{t('agents.tag.user')}</Tag>}
</AgentItem> </AgentItem>
))} ))}
</Container> </Container>
@@ -97,7 +123,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
} }
const Container = styled.div` const Container = styled.div`
padding: 0 12px;
height: 50vh; height: 50vh;
margin-top: 10px;
overflow-y: auto; overflow-y: auto;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
@@ -109,12 +137,14 @@ const AgentItem = styled.div`
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px; padding: 10px 15px;
border-radius: 8px; border-radius: 8px;
user-select: none; user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px; margin-bottom: 8px;
cursor: pointer; cursor: pointer;
&.default {
background-color: var(--color-background-soft);
}
.anticon { .anticon {
font-size: 16px; font-size: 16px;
color: var(--color-icon); color: var(--color-icon);
@@ -124,6 +154,18 @@ const AgentItem = styled.div`
} }
` `
const SearchIcon = styled.div`
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
margin-right: 6px;
`
export default class AddAssistantPopup { export default class AddAssistantPopup {
static topviewId = 0 static topviewId = 0
static hide() { static hide() {

View File

@@ -3,7 +3,7 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env' import { isLocalAi, UserAvatar } from '@renderer/config/env'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime, useShowAssistants } from '@renderer/hooks/useStore' import { useRuntime } from '@renderer/hooks/useStore'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -17,7 +17,6 @@ const Sidebar: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const avatar = useAvatar() const avatar = useAvatar()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { toggleShowAssistants } = useShowAssistants()
const { generating } = useRuntime() const { generating } = useRuntime()
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
@@ -38,23 +37,23 @@ const Sidebar: FC = () => {
navigate(path) navigate(path)
} }
const onToggleShowAssistants = () => {
pathname === '/' ? toggleShowAssistants() : navigate('/')
}
return ( return (
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBgColor }}> <Container
style={{
backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBgColor,
zIndex: minappShow ? 10000 : 'initial'
}}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<StyledLink onClick={onToggleShowAssistants}> <StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}> <Icon className={isRoute('/')}>
<i className="iconfont icon-chat"></i> <i className="iconfont icon-chat" />
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink onClick={() => to('/agents')}> <StyledLink onClick={() => to('/agents')}>
<Icon className={isRoute('/agents')}> <Icon className={isRoute('/agents')}>
<i className="iconfont icon-business-smart-assistant"></i> <i className="iconfont icon-business-smart-assistant" />
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink onClick={() => to('/translate')}> <StyledLink onClick={() => to('/translate')}>
@@ -64,7 +63,7 @@ const Sidebar: FC = () => {
</StyledLink> </StyledLink>
<StyledLink onClick={() => to('/apps')}> <StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}> <Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore"></i> <i className="iconfont icon-appstore" />
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink onClick={() => to('/files')}> <StyledLink onClick={() => to('/files')}>
@@ -77,7 +76,7 @@ const Sidebar: FC = () => {
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}> <StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}> <Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
<i className="iconfont icon-setting"></i> <i className="iconfont icon-setting" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Menus> </Menus>
@@ -97,12 +96,11 @@ const Container = styled.div`
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
margin-top: ${isMac ? 'var(--navbar-height)' : 0}; margin-top: ${isMac ? 'var(--navbar-height)' : 0};
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
z-index: 10000;
` `
const AvatarImg = styled(Avatar)` const AvatarImg = styled(Avatar)`
width: 28px; width: 32px;
height: 28px; height: 32px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin-bottom: ${isMac ? '12px' : '12px'}; margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: ${isMac ? '5px' : '2px'}; margin-top: ${isMac ? '5px' : '2px'};
@@ -126,10 +124,11 @@ const Icon = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 6px; border-radius: 50%;
margin-bottom: 5px; margin-bottom: 5px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
-webkit-app-region: none; -webkit-app-region: none;
transition: all 0.2s ease;
.iconfont, .iconfont,
.anticon { .anticon {
color: var(--color-icon); color: var(--color-icon);

View File

@@ -23,7 +23,8 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
} }
}, },
token: { token: {
colorPrimary: '#00b96b' colorPrimary: '#00b96b',
borderRadius: 6
} }
}}> }}>
{children} {children}

View File

@@ -61,11 +61,12 @@ const resources = {
'reset.double.confirm.content': 'All data will be lost, do you want to continue?', 'reset.double.confirm.content': 'All data will be lost, do you want to continue?',
'upgrade.success.title': 'Upgrade successfully', 'upgrade.success.title': 'Upgrade successfully',
'upgrade.success.content': 'Please restart the application to complete the upgrade', 'upgrade.success.content': 'Please restart the application to complete the upgrade',
'upgrade.success.button': 'Restart' 'upgrade.success.button': 'Restart',
'topic.added': 'New topic added'
}, },
chat: { chat: {
save: 'Save', save: 'Save',
'default.name': 'Default Assistant', 'default.name': '⭐️ Default Assistant',
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away", 'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
'default.topic.name': 'Default Topic', 'default.topic.name': 'Default Topic',
'topics.title': 'Topics', 'topics.title': 'Topics',
@@ -108,6 +109,11 @@ const resources = {
'message.new.branch': 'New Branch', 'message.new.branch': 'New Branch',
'assistant.search.placeholder': 'Search' 'assistant.search.placeholder': 'Search'
}, },
assistants: {
title: 'Assistants',
abbr: 'Assistant',
search: 'Search assistants...'
},
files: { files: {
title: 'Files', title: 'Files',
file: 'File', file: 'File',
@@ -333,11 +339,12 @@ const resources = {
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?', 'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?',
'upgrade.success.title': '升级成功', 'upgrade.success.title': '升级成功',
'upgrade.success.content': '重启应用以完成升级', 'upgrade.success.content': '重启应用以完成升级',
'upgrade.success.button': '重启' 'upgrade.success.button': '重启',
'topic.added': '话题添加成功'
}, },
chat: { chat: {
save: '保存', save: '保存',
'default.name': '默认助手', 'default.name': '⭐️ 默认助手',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。', 'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题', 'default.topic.name': '默认话题',
'topics.title': '话题', 'topics.title': '话题',
@@ -376,11 +383,16 @@ const resources = {
'settings.set_as_default': '应用到默认助手', 'settings.set_as_default': '应用到默认助手',
'settings.max': '不限', 'settings.max': '不限',
'suggestions.title': '建议的问题', 'suggestions.title': '建议的问题',
'add.assistant.title': '添加智能体', 'add.assistant.title': '添加助手',
'message.new.context': '清除上下文', 'message.new.context': '清除上下文',
'message.new.branch': '新分支', 'message.new.branch': '新分支',
'assistant.search.placeholder': '搜索' 'assistant.search.placeholder': '搜索'
}, },
assistants: {
title: '助手',
abbr: '助手',
search: '搜索助手'
},
files: { files: {
title: '文件', title: '文件',
file: '文件', file: '文件',

View File

@@ -1,4 +1,4 @@
import { DeleteOutlined, EditOutlined, MinusCircleOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon' import CopyIcon from '@renderer/components/Icons/CopyIcon'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup' import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
@@ -20,13 +20,20 @@ import styled from 'styled-components'
interface Props { interface Props {
activeAssistant: Assistant activeAssistant: Assistant
setActiveAssistant: (assistant: Assistant) => void setActiveAssistant: (assistant: Assistant) => void
onCreateDefaultAssistant: () => void
onCreateAssistant: () => void onCreateAssistant: () => void
} }
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => { const Assistants: FC<Props> = ({
activeAssistant,
setActiveAssistant,
onCreateAssistant,
onCreateDefaultAssistant
}) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants() const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const generating = useAppSelector((state) => state.runtime.generating) const generating = useAppSelector((state) => state.runtime.generating)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [dragging, setDragging] = useState(false)
const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id) const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id)
const { clickAssistantToShowTopic, topicPosition } = useSettings() const { clickAssistantToShowTopic, topicPosition } = useSettings()
const searchRef = useRef<InputRef>(null) const searchRef = useRef<InputRef>(null)
@@ -36,10 +43,10 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
const onDelete = useCallback( const onDelete = useCallback(
(assistant: Assistant) => { (assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id)) const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant() _assistant ? setActiveAssistant(_assistant) : onCreateDefaultAssistant()
removeAssistant(assistant.id) removeAssistant(assistant.id)
}, },
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant] [assistants, onCreateDefaultAssistant, removeAssistant, setActiveAssistant]
) )
const onEditAssistant = useCallback( const onEditAssistant = useCallback(
@@ -175,24 +182,38 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
/> />
</SearchContainer> </SearchContainer>
)} )}
<DragableList list={list} onUpdate={updateAssistants} droppableProps={{ isDropDisabled: !isEmpty(search) }}> <DragableList
list={list}
onUpdate={updateAssistants}
droppableProps={{ isDropDisabled: !isEmpty(search) }}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => { {(assistant) => {
const isCurrent = assistant.id === activeAssistant?.id const isCurrent = assistant.id === activeAssistant?.id
return ( return (
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}> <Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}> <AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}>
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName> <AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
<ArrowRightButton {isCurrent && (
className={`arrow-button ${isCurrent ? 'active' : ''}`} <ArrowRightButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}> <i className="iconfont icon-gridlines" />
<i className="iconfont icon-gridlines" /> </ArrowRightButton>
</ArrowRightButton> )}
{false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>} {false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>}
</AssistantItem> </AssistantItem>
</Dropdown> </Dropdown>
) )
}} }}
</DragableList> </DragableList>
{!dragging && (
<AssistantItem onClick={onCreateAssistant}>
<AssistantName>
<PlusOutlined style={{ color: 'var(--color-text-2)', marginRight: 4 }} />
{t('chat.add.assistant.title')}
</AssistantName>
</AssistantItem>
)}
</Container> </Container>
) )
} }
@@ -215,12 +236,15 @@ const AssistantItem = styled.div`
border-radius: 4px; border-radius: 4px;
margin: 0 10px; margin: 0 10px;
padding-right: 35px; padding-right: 35px;
cursor: pointer;
font-family: Ubuntu; font-family: Ubuntu;
cursor: pointer;
.iconfont { .iconfont {
opacity: 0; opacity: 0;
color: var(--color-text-3); color: var(--color-text-3);
} }
&:hover {
background-color: var(--color-background-soft);
}
&.active { &.active {
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
.name { .name {

View File

@@ -21,7 +21,7 @@ const HomePage: FC = () => {
return ( return (
<Container> <Container>
<Navbar activeAssistant={activeAssistant} setActiveAssistant={setActiveAssistant} activeTopic={activeTopic} /> <Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<ContentContainer> <ContentContainer>
{showAssistants && ( {showAssistants && (
<RightSidebar <RightSidebar

View File

@@ -4,7 +4,6 @@ import {
FormOutlined, FormOutlined,
FullscreenExitOutlined, FullscreenExitOutlined,
FullscreenOutlined, FullscreenOutlined,
HistoryOutlined,
PauseCircleOutlined, PauseCircleOutlined,
QuestionCircleOutlined QuestionCircleOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
@@ -258,11 +257,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
textareaRef.current?.focus() textareaRef.current?.focus()
}, [assistant]) }, [assistant])
useEffect(() => {
document.addEventListener('paste', onPaste)
return () => document.removeEventListener('paste', onPaste)
}, [onPaste])
return ( return (
<Container> <Container>
<AttachmentPreview files={files} setFiles={setFiles} /> <AttachmentPreview files={files} setFiles={setFiles} />
@@ -283,6 +277,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
onBlur={() => setInputFocus(false)} onBlur={() => setInputFocus(false)}
onInput={onInput} onInput={onInput}
disabled={searching} disabled={searching}
onPaste={(e) => onPaste(e.nativeEvent)}
onClick={() => searching && dispatch(setSearching(false))} onClick={() => searching && dispatch(setSearching(false))}
/> />
<Toolbar> <Toolbar>
@@ -305,16 +300,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton> </ToolbarButton>
</Popconfirm> </Popconfirm>
</Tooltip> </Tooltip>
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
<ToolbarButton
type="text"
onClick={() => {
!showTopics && toggleShowTopics()
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow> <Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton <ToolbarButton
type="text" type="text"
@@ -421,6 +406,10 @@ const ToolbarButton = styled(Button)`
transition: all 0.3s ease; transition: all 0.3s ease;
color: var(--color-icon); color: var(--color-icon);
} }
.icon-a-addchat {
font-size: 19px;
margin-bottom: -2px;
}
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
.anticon, .anticon,

View File

@@ -1,5 +1,5 @@
import FileManager from '@renderer/services/file'
import { FileTypes, Message } from '@renderer/types' import { FileTypes, Message } from '@renderer/types'
import { getFileDirectory } from '@renderer/utils'
import { Image as AntdImage, Upload } from 'antd' import { Image as AntdImage, Upload } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@@ -9,6 +9,10 @@ interface Props {
} }
const MessageAttachments: FC<Props> = ({ message }) => { const MessageAttachments: FC<Props> = ({ message }) => {
if (!message.files) {
return null
}
if (message?.files && message.files[0]?.type === FileTypes.IMAGE) { if (message?.files && message.files[0]?.type === FileTypes.IMAGE) {
return ( return (
<Container> <Container>
@@ -22,10 +26,9 @@ const MessageAttachments: FC<Props> = ({ message }) => {
<Upload <Upload
listType="picture" listType="picture"
disabled disabled
onPreview={(item) => item.url && window.open(getFileDirectory(item.url))}
fileList={message.files?.map((file) => ({ fileList={message.files?.map((file) => ({
uid: file.id, uid: file.id,
url: 'file://' + file.path, url: 'file://' + FileManager.getSafePath(file),
status: 'done', status: 'done',
name: file.origin_name name: file.origin_name
}))} }))}

View File

@@ -1,16 +1,19 @@
import { FormOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup' import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { syncAsistantToAgent } from '@renderer/services/assistant' import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Switch } from 'antd' import { Switch } from 'antd'
import { FC, useCallback } from 'react' import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton' import SelectModelButton from './components/SelectModelButton'
@@ -18,20 +21,16 @@ import SelectModelButton from './components/SelectModelButton'
interface Props { interface Props {
activeAssistant: Assistant activeAssistant: Assistant
activeTopic: Topic activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void setActiveTopic: (topic: Topic) => void
} }
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant }) => { const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
const { assistant, updateAssistant } = useAssistant(activeAssistant.id) const { assistant, updateAssistant, addTopic } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { theme, toggleTheme } = useTheme() const { theme, toggleTheme } = useTheme()
const { topicPosition } = useSettings() const { topicPosition } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation()
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
assistant && setActiveAssistant(assistant)
}
const onEditAssistant = useCallback(async () => { const onEditAssistant = useCallback(async () => {
const _assistant = await AssistantSettingPopup.show({ assistant }) const _assistant = await AssistantSettingPopup.show({ assistant })
@@ -39,6 +38,15 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant }) => {
syncAsistantToAgent(_assistant) syncAsistantToAgent(_assistant)
}, [assistant, updateAssistant]) }, [assistant, updateAssistant])
const addNewTopic = useCallback(() => {
const topic = getDefaultTopic()
addTopic(topic)
setActiveTopic(topic)
db.topics.add({ id: topic.id, messages: [] })
window.message.success({ content: t('message.topic.added'), key: 'topic-added' })
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, setActiveTopic, t])
return ( return (
<Navbar> <Navbar>
{showAssistants && ( {showAssistants && (
@@ -46,8 +54,8 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant }) => {
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}> <NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
<i className="iconfont icon-hide-sidebar" /> <i className="iconfont icon-hide-sidebar" />
</NewButton> </NewButton>
<NewButton onClick={onCreateAssistant}> <NewButton onClick={addNewTopic}>
<i className="iconfont icon-a-addchat" /> <FormOutlined />
</NewButton> </NewButton>
</NavbarLeft> </NavbarLeft>
)} )}
@@ -85,7 +93,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant }) => {
export const NewButton = styled.div` export const NewButton = styled.div`
-webkit-app-region: none; -webkit-app-region: none;
border-radius: 4px; border-radius: 8px;
height: 30px; height: 30px;
padding: 0 7px; padding: 0 7px;
display: flex; display: flex;

View File

@@ -1,4 +1,5 @@
import { BarsOutlined, SettingOutlined } from '@ant-design/icons' import { BarsOutlined, SettingOutlined } from '@ant-design/icons'
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore' import { useShowTopics } from '@renderer/hooks/useStore'
@@ -43,12 +44,18 @@ const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssist
} }
const showTab = !(position === 'left' && topicPosition === 'right') const showTab = !(position === 'left' && topicPosition === 'right')
const assistantTab = { const assistantTab = {
label: t('common.assistant'), label: t('assistants.abbr'),
value: 'assistants', value: 'assistants',
icon: <i className="iconfont icon-business-smart-assistant" /> icon: <i className="iconfont icon-business-smart-assistant" />
} }
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
assistant && setActiveAssistant(assistant)
}
const onCreateDefaultAssistant = () => { const onCreateDefaultAssistant = () => {
const assistant = { ...defaultAssistant, id: uuid() } const assistant = { ...defaultAssistant, id: uuid() }
addAssistant(assistant) addAssistant(assistant)
@@ -95,8 +102,16 @@ const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssist
options={ options={
[ [
position === 'left' && topicPosition === 'left' ? assistantTab : undefined, position === 'left' && topicPosition === 'left' ? assistantTab : undefined,
{ label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> }, {
{ label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> } label: t('common.topics'),
value: 'topic',
icon: <BarsOutlined />
},
{
label: t('settings.title'),
value: 'settings',
icon: <SettingOutlined />
}
].filter(Boolean) as SegmentedProps['options'] ].filter(Boolean) as SegmentedProps['options']
} }
onChange={(value) => setTab(value as 'topic' | 'settings')} onChange={(value) => setTab(value as 'topic' | 'settings')}
@@ -108,7 +123,8 @@ const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssist
<Assistants <Assistants
activeAssistant={activeAssistant} activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant} setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateDefaultAssistant} onCreateAssistant={onCreateAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
/> />
)} )}
{tab === 'topic' && ( {tab === 'topic' && (

View File

@@ -136,8 +136,11 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
return ( return (
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}> <Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)}> <TopicListItem className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)}>
<TopicName>{topic.name}</TopicName> <TopicName className="name">
{assistant.topics.length > 1 && ( <TopicHash>#</TopicHash>
{topic.name.replace('`', '')}
</TopicName>
{assistant.topics.length > 1 && isActive && (
<MenuButton <MenuButton
className="menu" className="menu"
onClick={(e) => { onClick={(e) => {
@@ -162,13 +165,15 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
padding-top: 10px; padding-top: 10px;
overflow-y: scroll; overflow-y: scroll;
max-height: calc(100vh - var(--navbar-height) - 140px); max-height: calc(100vh - var(--navbar-height) - 70px);
&::-webkit-scrollbar {
display: none;
}
` `
const TopicListItem = styled.div` const TopicListItem = styled.div`
padding: 7px 10px; padding: 7px 10px;
margin: 0 10px; margin: 0 10px;
cursor: pointer;
border-radius: 4px; border-radius: 4px;
font-family: Ubuntu; font-family: Ubuntu;
font-size: 13px; font-size: 13px;
@@ -177,13 +182,24 @@ const TopicListItem = styled.div`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
position: relative; position: relative;
font-family: Ubuntu;
cursor: pointer;
.menu { .menu {
opacity: 0; opacity: 0;
color: var(--color-text-3); color: var(--color-text-3);
} }
&:hover {
background-color: var(--color-background-soft);
.name {
opacity: 1;
}
}
&.active { &.active {
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
font-weight: 500; .name {
opacity: 1;
font-weight: 500;
}
.menu { .menu {
opacity: 1; opacity: 1;
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
@@ -195,12 +211,12 @@ const TopicListItem = styled.div`
` `
const TopicName = styled.div` const TopicName = styled.div`
color: var(--color-text);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 13px; font-size: 13px;
opacity: 0.6;
` `
const MenuButton = styled.div` const MenuButton = styled.div`
@@ -208,17 +224,20 @@ const MenuButton = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 30px; min-width: 22px;
height: 24px; min-height: 22px;
min-width: 24px;
min-height: 24px;
border-radius: 4px;
position: absolute; position: absolute;
right: 10px; right: 8px;
top: 5px; top: 6px;
.anticon { .anticon {
font-size: 12px; font-size: 12px;
} }
` `
const TopicHash = styled.span`
font-size: 13px;
color: var(--color-text-3);
margin-right: 2px;
`
export default Topics export default Topics

View File

@@ -124,16 +124,25 @@ export default class OpenAIProvider extends BaseProvider {
userMessages.push(await this.getMessageParam(message, model)) userMessages.push(await this.getMessageParam(message, model))
} }
const isSupportStreamOutput = this.isSupportStreamOutput(model.id)
// @ts-ignore key is not typed // @ts-ignore key is not typed
const stream = await this.sdk.chat.completions.create({ const stream = await this.sdk.chat.completions.create({
model: model.id, model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[], messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: this.isSupportStreamOutput(model.id), stream: isSupportStreamOutput,
temperature: assistant?.settings?.temperature, temperature: assistant?.settings?.temperature,
max_tokens: maxTokens, max_tokens: maxTokens,
keep_alive: this.keepAliveTime keep_alive: this.keepAliveTime
}) })
if (!isSupportStreamOutput) {
return onChunk({
text: stream.choices[0].message?.content || '',
usage: stream.usage
})
}
for await (const chunk of stream) { for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) { if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
break break

View File

@@ -77,7 +77,7 @@ export async function fetchChatCompletion({
message.status = 'success' message.status = 'success'
if (!message.usage) { if (!message.usage || !message?.usage?.completion_tokens) {
message.usage = await estimateMessagesUsage({ message.usage = await estimateMessagesUsage({
assistant, assistant,
messages: [..._messages, message] messages: [..._messages, message]

View File

@@ -55,7 +55,7 @@ class FileManager {
} }
static isDangerFile(file: FileType) { static isDangerFile(file: FileType) {
return ['.sh', '.bat', '.cmd', '.ps1'].includes(file.ext) return ['.sh', '.bat', '.cmd', '.ps1', '.vbs', 'reg'].includes(file.ext)
} }
static getSafePath(file: FileType) { static getSafePath(file: FileType) {