Compare commits

...

12 Commits

Author SHA1 Message Date
kangfenmao
8d7cde1231 0.3.2 2024-07-22 14:52:54 +08:00
kangfenmao
87c04408de feat: add contextCount to inputbar 2024-07-22 14:50:40 +08:00
kangfenmao
2592448c74 feat: add email to about titles 2024-07-22 14:26:35 +08:00
kangfenmao
6f054874e8 chore: remove change log component 2024-07-22 14:25:15 +08:00
kangfenmao
40d687104e feat: new about page 2024-07-22 14:24:14 +08:00
kangfenmao
ac3cfe2878 fix: disable switch while assistant generating message 2024-07-22 11:28:26 +08:00
kangfenmao
e9a7735fce feat: add updateAssistantSettings to useAssistant hook 2024-07-22 11:15:10 +08:00
kangfenmao
c1a8198575 fix(ProviderSDK): clarify instruction for session summary to avoid punctuation marks and special characters 2024-07-22 10:49:10 +08:00
kangfenmao
8b45548b79 refactor: topic component code 2024-07-22 10:38:00 +08:00
kangfenmao
3f3b930819 fix: disabled switch topic while generating message 2024-07-22 10:22:47 +08:00
kangfenmao
a5d6e2c5c5 0.3.1 2024-07-21 23:44:09 +08:00
kangfenmao
2993ab8dc1 fix: topic missing bug and delete assistant crash 2024-07-21 23:43:17 +08:00
20 changed files with 311 additions and 441 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.3.0",
"version": "0.3.2",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",

View File

@@ -1,55 +0,0 @@
# CHANGES LOG
### v0.3.0 - 2024-07-21
- Supports setting the model Temperature parameter
- Support for setting the number of contexts
- Token consumption estimation added to the input box
### v0.2.9 - 2024-07-20
- 📢 Add AiHubMix provider
### v0.2.8 - 2024-07-20
- 🆕 Feature: Add customized service providers
### v0.2.7 - 2024-07-19
- 📢 Add DashScope Provider
- 📢 Add Anthropic Provider
### v0.2.6 - 2024-07-17
- 🆕 Fixed the issue of the BaiChuan API KEY not displaying when clicking to obtain the URL
- 📢 New intelligent body center style
### v0.2.5 - 2024-07-17
- 🆕 Baichuan AI Service Providers
- 📢 New Intelligent Agent Page with Multiple Professional Assistants
- 🌐 Multilingual Issue Fixes and Detailed Optimizations
### v0.2.4 - 2024-07-16
- Fixed the issue of the update log page cannot be scrolled
- Added a check for updates button
### v0.2.3 - 2024-07-16
- Fixed multi-language prompt errors
- Fixed default model error issues with ZHIPU AI
- Fixed OpenRouter API detection error issues
- Fixed multi-language translation errors with model providers
### v0.2.2 - 2024-07-15
- Fix the issue where the default assistant name is empty.
- Fix the problem with default language detection during the first installation.
- Adjust the changelog style.
### v0.2.1 - 2024-07-15
- **Feature**: Add new feature for pausing message sending
- **Fix**: Resolve incomplete translation issue upon language switch
- **Build**: Support for macOS Intel architecture

View File

@@ -1,56 +0,0 @@
# 更新日志
### v0.3.0 - 2024-07-21
- 支持设置模型 Temperature 参数
- 支持设置上下文数量
- 输入框增加 Token 消耗预估
### v0.2.9 - 2024-07-20
- 📢 新增 AiMixHub 服务提供商
### v0.2.8 - 2024-07-20
- 🆕 新功能: 可以添加自定义服务提供商了
### v0.2.7 - 2024-07-19
- 📢 新增阿里云灵积服务商
- 📢 新增 Anthropic 服务商
### v0.2.6 - 2024-07-17
- 🆕 修复百川 API KEY 点击获取网址没有显示问题
- 📢 新的智能体中心样式
### v0.2.5 - 2024-07-17
- 🆕 新增百川AI服务商
- 📢 全新的智能体页面,新增多种职业助手
- 🌐 多语言问题修复,细节优化
### v0.2.4 - 2024-07-16
- 修复更新日志页面不能滚动问题
- 新增检查更新按钮
### v0.2.3 - 2024-07-16
- 修复多语言提示错误
- 修复智谱AI默认模型错误问题
- 修复 OpenRouter API 检测出错问题
- 修复模型提供商多语言翻译错误问题
### v0.2.2 - 2024-07-15
- 修复默认助理名称为空的问题
- 修复首次安装默认语言检测问题
- 更新日志样式微调
### v0.2.1 - 2024-07-15
- 【功能】新增消息暂停发送功能
- 【修复】修复多语言切换不彻底问题
- 【构建】支持 macOS Intel 架构

View File

@@ -1,4 +1,4 @@
import { useAppInitEffect } from '@renderer/hooks/useAppInitEffect'
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd'
import { findIndex, pullAt } from 'lodash'
import React, { useEffect, useState } from 'react'
@@ -29,7 +29,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
const [messageApi, messageContextHolder] = message.useMessage()
const [modal, modalContextHolder] = Modal.useModal()
useAppInitEffect()
useAppInit()
onPop = () => {
const views = [...elements]

View File

@@ -5,7 +5,7 @@ import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
export function useAppInitEffect() {
export function useAppInit() {
const dispatch = useAppDispatch()
useEffect(() => {

View File

@@ -1,20 +1,21 @@
import { getDefaultTopic } from '@renderer/services/assistant'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addTopic as _addTopic,
removeAllTopics as _removeAllTopics,
removeTopic as _removeTopic,
setModel as _setModel,
updateAssistants as _updateAssistants,
updateDefaultAssistant as _updateDefaultAssistant,
updateTopic as _updateTopic,
updateTopics as _updateTopics,
addAssistant,
addTopic,
removeAllTopics,
removeAssistant,
updateAssistant
removeTopic,
setModel,
updateAssistant,
updateAssistants,
updateAssistantSettings,
updateDefaultAssistant,
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm'
import { Assistant, Model, Topic } from '@renderer/types'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import localforage from 'localforage'
export function useAssistants() {
@@ -23,9 +24,8 @@ export function useAssistants() {
return {
assistants,
updateAssistants: (assistants: Assistant[]) => dispatch(_updateAssistants(assistants)),
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
removeAssistant: (id: string) => {
dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id)
@@ -44,17 +44,21 @@ export function useAssistant(id: string) {
return {
assistant,
model: assistant?.model ?? defaultModel,
addTopic: (topic: Topic) => dispatch(_addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => dispatch(_removeTopic({ assistantId: assistant.id, topic })),
updateTopic: (topic: Topic) => dispatch(_updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(_updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(_removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(_setModel({ assistantId: assistant.id, model }))
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => dispatch(removeTopic({ assistantId: assistant.id, topic })),
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
updateAssistantSettings: (settings: AssistantSettings) => {
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
}
}
}
export function useDefaultAssistant() {
const { defaultAssistant } = useAppSelector((state) => state.assistants)
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const dispatch = useAppDispatch()
return {
@@ -62,7 +66,7 @@ export function useDefaultAssistant() {
...defaultAssistant,
topics: [getDefaultTopic()]
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(_updateDefaultAssistant({ assistant }))
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}
}

View File

@@ -1,11 +1,11 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addModel as _addModel,
removeModel as _removeModel,
updateProvider as _updateProvider,
updateProviders as _updateProviders,
addModel,
addProvider,
removeProvider
removeModel,
removeProvider,
updateProvider,
updateProviders
} from '@renderer/store/llm'
import { Assistant, Model, Provider } from '@renderer/types'
import { useDefaultModel } from './useAssistant'
@@ -24,8 +24,8 @@ export function useProviders() {
providers,
addProvider: (provider: Provider) => dispatch(addProvider(provider)),
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
updateProviders: (providers: Provider[]) => dispatch(_updateProviders(providers))
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
updateProviders: (providers: Provider[]) => dispatch(updateProviders(providers))
}
}
@@ -48,9 +48,9 @@ export function useProvider(id: string) {
return {
provider,
models: provider.models,
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
addModel: (model: Model) => dispatch(_addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(_removeModel({ providerId: id, model }))
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
}
}

View File

@@ -41,7 +41,8 @@ const resources = {
'error.enter.model': 'Please select a model first',
'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful',
'chat.completion.paused': 'Chat completion paused'
'chat.completion.paused': 'Chat completion paused',
'switch.disabled': 'Switching is disabled while the assistant is generating'
},
assistant: {
'default.name': '😀 Default Assistant',
@@ -66,7 +67,7 @@ const resources = {
'input.send': 'Send',
'input.pause': 'Pause',
'input.settings': 'Settings',
'input.estimated_tokens': 'Estimated tokens: ',
'input.estimated_tokens': 'Estimated Tokens: ',
'settings.temperature': 'Temperature',
'settings.temperature.tip':
'Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.',
@@ -96,11 +97,11 @@ const resources = {
},
settings: {
title: 'Settings',
general: 'General',
general: 'General Settings',
provider: 'Model Provider',
model: 'Model Settings',
assistant: 'Default Assistant',
about: 'About',
about: 'About & Feedback',
'general.title': 'General Settings',
'provider.api_key': 'API Key',
'provider.check': 'Check',
@@ -132,7 +133,16 @@ const resources = {
'provider.delete.title': 'Delete Provider',
'provider.delete.content': 'Are you sure you want to delete this provider?',
'provider.edit.name': 'Provider Name',
'provider.edit.name.placeholder': 'Example: OpenAI'
'provider.edit.name.placeholder': 'Example: OpenAI',
'about.title': 'About',
'about.releases.title': '📔 Release Notes',
'about.releases.button': 'Releases',
'about.website.title': '🌐 Official Website',
'about.website.button': 'Website',
'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact',
'about.contact.button': 'Email'
}
}
},
@@ -174,7 +184,8 @@ const resources = {
'error.enter.model': '请选择一个模型',
'api.connection.failed': '连接失败',
'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止'
'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换'
},
assistant: {
'default.name': '😃 默认助手 - Assistant',
@@ -234,7 +245,7 @@ const resources = {
provider: '模型提供商',
model: '模型设置',
assistant: '默认助手',
about: '关于',
about: '关于我们',
'general.title': '常规设置',
'provider.api_key': 'API 密钥',
'provider.check': '检查',
@@ -257,7 +268,7 @@ const resources = {
'models.empty': '没有模型',
'assistant.title': '默认助手',
'assistant.model_params': '模型参数',
'about.description': '一为创造者而生的 AI 助手',
'about.description': '一为创造者而生的 AI 助手',
'about.updateNotAvailable': '你的软件已是最新版本',
'about.checkingUpdate': '正在检查更新...',
'about.updateError': '更新出错',
@@ -266,7 +277,16 @@ const resources = {
'provider.delete.title': '删除提供商',
'provider.delete.content': '确定要删除此模型提供商吗?',
'provider.edit.name': '模型提供商名称',
'provider.edit.name.placeholder': '例如 OpenAI'
'provider.edit.name.placeholder': '例如 OpenAI',
'about.title': '关于我们',
'about.releases.title': '📔 更新日志',
'about.releases.button': '查看',
'about.website.title': '🌐 官方网站',
'about.website.button': '查看',
'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系',
'about.contact.button': '邮件'
}
}
}

View File

@@ -1,6 +1,6 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Button, Col, InputNumber, Popover, Row, Slider, Tooltip } from 'antd'
import { debounce } from 'lodash'
@@ -12,26 +12,26 @@ interface Props {
assistant: Assistant
}
const PopoverContent: FC<Props> = ({ assistant }) => {
const { updateAssistant } = useAssistants()
const [temperature, setTemperature] = useState(assistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const PopoverContent: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const { t } = useTranslation()
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
updateAssistant({
...assistant,
settings: {
updateAssistantSettings({
...assistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
}
})
},
1000,
{ leading: false, trailing: true }
{
leading: false,
trailing: true
}
),
[]
)
@@ -64,8 +64,8 @@ const PopoverContent: FC<Props> = ({ assistant }) => {
}
useEffect(() => {
setTemperature(assistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
}, [assistant])
return (

View File

@@ -1,13 +1,15 @@
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useAppSelector } from '@renderer/store'
import { Assistant } from '@renderer/types'
import { droppableReorder, uuid } from '@renderer/utils'
import { Dropdown, MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { last } from 'lodash'
import { FC, useRef } from 'react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -18,8 +20,9 @@ interface Props {
}
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
const targetAssistant = useRef<Assistant | null>(null)
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const { updateAssistant } = useAssistant(activeAssistant.id)
const generating = useAppSelector((state) => state.runtime.generating)
const { t } = useTranslation()
@@ -29,26 +32,25 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
removeAssistant(assistant.id)
}
const items: MenuProps['items'] = [
const getMenuItems = (assistant: Assistant) =>
[
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
if (targetAssistant.current) {
const _assistant = await AssistantSettingPopup.show({ assistant: targetAssistant.current })
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
}
}
},
{
label: t('common.duplicate'),
key: 'duplicate',
icon: <CopyOutlined />,
async onClick() {
const assistant: Assistant = { ...activeAssistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(assistant)
setActiveAssistant(assistant)
onClick: async () => {
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(_assistant)
setActiveAssistant(_assistant)
}
},
{ type: 'divider' },
@@ -57,11 +59,9 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
targetAssistant.current && onDelete(targetAssistant.current)
onClick: () => onDelete(assistant)
}
}
]
] as ItemType[]
const onDragEnd = (result: DropResult) => {
if (result.destination) {
@@ -72,6 +72,14 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
}
}
const onSwitchAssistant = (assistant: Assistant) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
setActiveAssistant(assistant)
}
return (
<Container>
<DragDropContext onDragEnd={onDragEnd}>
@@ -82,13 +90,9 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown
key={assistant.id}
menu={{ items }}
trigger={['contextMenu']}
onOpenChange={() => (targetAssistant.current = assistant)}>
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<AssistantItem
onClick={() => setActiveAssistant(assistant)}
onClick={() => onSwitchAssistant(assistant)}
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
<AssistantName>{assistant.name}</AssistantName>
</AssistantItem>

View File

@@ -1,31 +1,32 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Topic } from '@renderer/types'
import { estimateInputTokenCount, uuid } from '@renderer/utils'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import { MoreOutlined } from '@ant-design/icons'
import { Button, Popconfirm, Tooltip } from 'antd'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { useAssistant } from '@renderer/hooks/useAssistant'
import {
ClearOutlined,
ControlOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
MoreOutlined,
PauseCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import { debounce, isEmpty } from 'lodash'
import SendMessageSetting from './SendMessageSetting'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
import store, { useAppSelector } from '@renderer/store'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useTranslation } from 'react-i18next'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Topic } from '@renderer/types'
import { estimateInputTokenCount, uuid } from '@renderer/utils'
import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
import { debounce, isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AssistantSettings from './AssistantSettings'
import SendMessageSetting from './SendMessageSetting'
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
interface Props {
assistant: Assistant
@@ -41,7 +42,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const generating = useAppSelector((state) => state.runtime.generating)
const inputRef = useRef<TextAreaRef>(null)
const { t } = useTranslation()
const sendMessage = () => {
@@ -86,7 +86,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
const addNewTopic = useCallback(() => {
const topic: Topic = getDefaultTopic()
const topic = getDefaultTopic()
addTopic(topic)
setActiveTopic(topic)
}, [addTopic, setActiveTopic])
@@ -194,6 +194,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
styles={{ textarea: { paddingLeft: 0 } }}
/>
<TextCount>
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} |{' '}
{t('assistant.input.estimated_tokens')}: {`${inputTokenCount}/${estimateTokenCount}`}
</TextCount>
</Container>
@@ -258,6 +259,7 @@ const TextCount = styled.div`
bottom: 8px;
font-size: 11px;
color: var(--color-text-3);
z-index: 10;
`
export default Inputbar

View File

@@ -105,7 +105,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
{message.usage && (
<>
<MessageMetadata>
Tokens: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
tokens: {message.usage.total_tokens} (in:{message.usage.prompt_tokens}/out:
{message.usage.completion_tokens})
</MessageMetadata>
</>

View File

@@ -65,7 +65,6 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
console.debug({ assistant, provider, message: msg, topic })
onSendMessage(msg)
fetchChatCompletion({ assistant, messages: [...messages, msg], topic, onResponse: setLastMessage })
}),

View File

@@ -4,13 +4,14 @@ import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { fetchMessagesSummary } from '@renderer/services/api'
import { Assistant, Topic } from '@renderer/types'
import { Button, Dropdown, MenuProps, Popconfirm } from 'antd'
import { FC, useRef } from 'react'
import { FC } from 'react'
import styled from 'styled-components'
import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons'
import LocalStorage from '@renderer/services/storage'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
import { useAppSelector } from '@renderer/store'
interface Props {
assistant: Assistant
@@ -18,25 +19,24 @@ interface Props {
setActiveTopic: (topic: Topic) => void
}
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null)
const { assistant, removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const topicMenuItems: MenuProps['items'] = [
const getTopicMenuItems = (topic: Topic) => {
const menus: MenuProps['items'] = [
{
label: t('assistant.topics.auto_rename'),
key: 'auto-rename',
icon: <SignatureOutlined />,
async onClick() {
if (currentTopic.current) {
const messages = await LocalStorage.getTopicMessages(currentTopic.current.id)
const messages = await LocalStorage.getTopicMessages(topic.id)
if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
updateTopic({ ...currentTopic.current, name: summaryText })
}
updateTopic({ ...topic, name: summaryText })
}
}
}
@@ -49,31 +49,33 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const name = await PromptPopup.show({
title: t('assistant.topics.edit.title'),
message: t('assistant.topics.edit.placeholder'),
defaultValue: currentTopic.current?.name || ''
defaultValue: topic?.name || ''
})
if (name && currentTopic.current && currentTopic.current?.name !== name) {
updateTopic({ ...currentTopic.current, name })
if (name && topic?.name !== name) {
updateTopic({ ...topic, name })
}
}
}
]
if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' })
topicMenuItems.push({
menus.push({ type: 'divider' })
menus.push({
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick() {
if (assistant.topics.length === 1) return
currentTopic.current && removeTopic(currentTopic.current)
currentTopic.current = null
removeTopic(topic)
setActiveTopic(assistant.topics[0])
}
})
}
return menus
}
const onDragEnd = (result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
@@ -82,6 +84,14 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
}
}
const onSwitchTopic = (topic: Topic) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
setActiveTopic(topic)
}
return (
<Container style={{ display: showRightSidebar ? 'block' : 'none' }}>
<TopicTitle>
@@ -108,14 +118,10 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown
menu={{ items: topicMenuItems }}
trigger={['contextMenu']}
key={topic.id}
onOpenChange={(open) => open && (currentTopic.current = topic)}>
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem
className={topic.id === activeTopic?.id ? 'active' : ''}
onClick={() => setActiveTopic(topic)}>
onClick={() => onSwitchTopic(topic)}>
{topic.name}
</TopicListItem>
</Dropdown>

View File

@@ -1,12 +1,12 @@
import { Avatar, Button, Progress } from 'antd'
import { Avatar, Button, Progress, Row, Tag } from 'antd'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png'
import { runAsyncFunction } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
import Changelog from './components/Changelog'
import { debounce } from 'lodash'
import { ProgressInfo } from 'electron-updater'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
const AboutSettings: FC = () => {
const [version, setVersion] = useState('')
@@ -26,8 +26,17 @@ const AboutSettings: FC = () => {
{ leading: true, trailing: false }
)
const onOpenWebsite = (suffix = '') => {
window.api.openWebsite('https://github.com/kangfenmao/cherry-studio' + suffix)
const onOpenWebsite = (url: string) => {
window.api.openWebsite(url)
}
const mailto = async () => {
const email = 'kangfenmao@qq.com'
const subject = 'Cherry Studio Feedback'
const version = (await window.api.getAppInfo()).version
const platform = window.electron.process.platform
const url = `mailto:${email}?subject=${subject}&body=%0A%0AVersion: ${version} | Platform: ${platform}`
onOpenWebsite(url)
}
useEffect(() => {
@@ -69,57 +78,92 @@ const AboutSettings: FC = () => {
}, [t])
return (
<Container>
<AvatarWrapper onClick={() => onOpenWebsite()}>
<SettingContainer>
<SettingTitle>{t('settings.about.title')}</SettingTitle>
<SettingDivider />
<AboutHeader>
<Row align="middle">
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
{percent > 0 && (
<ProgressCircle
type="circle"
size={104}
size={84}
percent={percent}
showInfo={false}
strokeLinecap="butt"
strokeColor="#67ad5b"
/>
)}
<Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} />
<Avatar src={Logo} size={80} style={{ minHeight: 80 }} />
</AvatarWrapper>
<Title>
Cherry Studio <Version onClick={() => onOpenWebsite('/releases')}>(v{version})</Version>
</Title>
<VersionWrapper>
<Title>Cherry Studio</Title>
<Description>{t('settings.about.description')}</Description>
<Tag
onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}
color="cyan"
style={{ marginTop: 8, cursor: 'pointer' }}>
v{version}
</Tag>
</VersionWrapper>
</Row>
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
</CheckUpdateButton>
<Changelog />
</Container>
</AboutHeader>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.releases.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}>
{t('settings.about.releases.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://easys.run/cherry-studio')}>
{t('settings.about.website.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.feedback.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues')}>
{t('settings.about.feedback.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}
const Container = styled.div`
const AboutHeader = styled.div`
display: flex;
flex: 1;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: calc(100vh - var(--navbar-height));
overflow-y: scroll;
padding: 0;
padding-bottom: 50px;
justify-content: space-between;
width: 100%;
padding: 5px 0;
`
const VersionWrapper = styled.div`
display: flex;
flex-direction: column;
min-height: 80px;
justify-content: center;
align-items: flex-start;
`
const Title = styled.div`
font-size: 20px;
font-weight: bold;
color: var(--color-text-1);
margin: 10px 0;
`
const Version = styled.span`
font-size: 14px;
color: var(--color-text-2);
margin: 10px 0;
text-align: center;
cursor: pointer;
margin-bottom: 5px;
`
const Description = styled.div`
@@ -128,18 +172,17 @@ const Description = styled.div`
text-align: center;
`
const CheckUpdateButton = styled(Button)`
margin-top: 10px;
`
const CheckUpdateButton = styled(Button)``
const AvatarWrapper = styled.div`
position: relative;
cursor: pointer;
margin-right: 15px;
`
const ProgressCircle = styled(Progress)`
position: absolute;
top: 48px;
top: -2px;
left: -2px;
`

View File

@@ -1,29 +0,0 @@
import changelogEn from '@renderer/CHANGELOG.en.md?raw'
import changelogZh from '@renderer/CHANGELOG.zh.md?raw'
import { FC } from 'react'
import Markdown from 'react-markdown'
import styled from 'styled-components'
import styles from './changelog.module.scss'
import i18n from '@renderer/i18n'
const Changelog: FC = () => {
const language = i18n.language
const changelog = language === 'zh-CN' ? changelogZh : changelogEn
return (
<Container>
<Markdown className={styles.markdown}>{changelog}</Markdown>
</Container>
)
}
const Container = styled.div`
font-size: 14px;
background-color: var(--color-background-soft);
margin-top: 40px;
padding: 20px;
border-radius: 5px;
width: 650px;
`
export default Changelog

View File

@@ -1,79 +0,0 @@
$background-color: #121212;
$text-color: #ffffff;
$heading-color: #00b96b;
$link-color: #3498db;
$code-background: #1e1e1e;
$code-color: #f0e7db;
.markdown {
body {
background-color: $background-color;
color: $text-color;
font-family: Arial, sans-serif;
padding: 20px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $heading-color;
}
h1 {
font-size: 22px;
font-weight: 700;
}
h3 {
margin: 10px 0;
font-weight: 500;
font-family: Arial, sans-serif;
}
a {
color: $link-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
strong {
font-weight: bold;
}
pre {
background-color: $code-background;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
code {
background-color: $code-background;
color: $code-color;
padding: 2px 4px;
border-radius: 3px;
}
blockquote {
border-left: 4px solid $heading-color;
padding-left: 10px;
margin-left: 0;
color: #b3b3b3;
}
ul,
ol {
padding-left: 20px;
list-style: disc;
}
li {
margin-bottom: 5px;
}
}

View File

@@ -47,7 +47,7 @@ export default class ProviderSDK {
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
max_tokens: 4096,
temperature: assistant.settings?.temperature
temperature: assistant?.settings?.temperature
})
.on('text', (text) => onChunk({ text: text || '' }))
.on('finalMessage', (message) =>
@@ -64,7 +64,7 @@ export default class ProviderSDK {
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true,
temperature: assistant.settings?.temperature
temperature: assistant?.settings?.temperature
})
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
@@ -83,7 +83,7 @@ export default class ProviderSDK {
const systemMessage = {
role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要标点符号'
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
}
if (this.isAnthropic) {

View File

@@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant'
import LocalStorage from '@renderer/services/storage'
import { Assistant, Model, Topic } from '@renderer/types'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { uniqBy } from 'lodash'
export interface AssistantsState {
@@ -33,6 +33,16 @@ const assistantsSlice = createSlice({
updateAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
},
updateAssistantSettings: (state, action: PayloadAction<{ assistantId: string; settings: AssistantSettings }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
settings: action.payload.settings
}
: assistant
)
},
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
@@ -111,7 +121,8 @@ export const {
updateTopic,
updateTopics,
removeAllTopics,
setModel
setModel,
updateAssistantSettings
} = assistantsSlice.actions
export default assistantsSlice.reducer

View File

@@ -169,10 +169,10 @@ export function getFirstCharacter(str) {
}
export const getAssistantSettings = (assistant: Assistant): AssistantSettings => {
const contextCount = assistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
return {
contextCount: contextCount === 20 ? 100000 : contextCount,
temperature: assistant.settings?.temperature ?? DEFAULT_TEMPERATURE
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE
}
}