Compare commits

...

12 Commits

Author SHA1 Message Date
kangfenmao
117069e450 chore(version): 0.3.0 2024-07-21 22:03:49 +08:00
kangfenmao
c5965dc696 fix: assistant settings bugs 2024-07-21 21:57:08 +08:00
kangfenmao
4169a2ef35 feat: add asistant model temperature maxTokens contextCount 2024-07-21 17:50:50 +08:00
kangfenmao
75c37632d4 feat: change default assistant name
# Conflicts:
#	src/renderer/src/i18n/index.ts
2024-07-21 10:51:33 +08:00
亢奋猫
3f5c151a11 Update README.md 2024-07-20 15:10:49 +08:00
kangfenmao
d049e36c46 0.2.9 2024-07-20 12:47:29 +08:00
kangfenmao
d05fc1c9be chore(version): v0.2.9 2024-07-20 12:47:19 +08:00
kangfenmao
f33317a3fb fix: send message setting position 2024-07-20 11:34:52 +08:00
kangfenmao
f2b5ed09c0 feat(provider): add AiHubMix provider 2024-07-20 11:29:24 +08:00
kangfenmao
81e66dde0e 0.2.8 2024-07-20 00:57:02 +08:00
kangfenmao
f76388d979 chore(version): v0.2.8 2024-07-20 00:56:52 +08:00
kangfenmao
9e542f813c feat: add custom llm provider 2024-07-20 00:50:46 +08:00
42 changed files with 882 additions and 116 deletions

View File

@@ -8,7 +8,7 @@
![](https://github.com/user-attachments/assets/18c10eed-4711-4975-bf9c-b274c61924f3)
![image.png](https://s2.loli.net/2024/07/16/IQPz12OajfNoBTV.png)
![](https://github.com/user-attachments/assets/7395ebf2-64f8-46fa-aa48-63293516c320)
# Feature

View File

@@ -56,5 +56,6 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
- 📢 新增阿里云灵积服务商
- 📢 新增 Anthropic 服务商
支持设置模型 Temperature 参数
支持设置上下文数量
输入框增加 Token 消耗预估

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.2.7",
"version": "0.3.0",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -56,6 +56,7 @@
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.0.0",
"gpt-tokens": "^1.3.6",
"i18next": "^23.11.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21",

View File

@@ -1,5 +1,19 @@
# 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

View File

@@ -1,5 +1,19 @@
# 更新日志
### 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
- 📢 新增阿里云灵积服务商

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -38,7 +38,7 @@
--topic-list-width: 250px;
--settings-width: var(--assistants-width);
--status-bar-height: 40px;
--input-bar-height: 120px;
--input-bar-height: 125px;
}
*,

View File

@@ -61,7 +61,11 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
{elements.map(({ element: Element, key }) =>
typeof Element === 'function' ? <Element key={`TOPVIEW_${key}`} /> : Element
typeof Element === 'function' ? (
<Element key={`TOPVIEW_${key}`} />
) : (
<div key={`TOPVIEW_${key}`}>{Element}</div>
)
)}
</div>
)}

View File

@@ -39,18 +39,20 @@ const NavbarLeftContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
font-weight: bold;
color: var(--color-text-1);
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
border-right: 1px solid var(--color-border);
padding: 0 20px;
font-size: 14px;
font-weight: bold;
color: var(--color-text-1);
text-align: center;
border-right: 1px solid var(--color-border);
padding: 0 20px;
`
const NavbarRightContainer = styled.div`

View File

@@ -44,14 +44,13 @@ const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
padding: 8px 0;
min-width: var(--sidebar-width);
min-height: 100%;
-webkit-app-region: drag !important;
background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border);
margin-top: var(--navbar-height);
padding-bottom: calc(var(--navbar-height) + 6px);
padding-top: var(--navbar-height);
`
const AvatarImg = styled.img`
@@ -60,6 +59,7 @@ const AvatarImg = styled.img`
height: 28px;
background-color: var(--color-background-soft);
margin: 5px 0;
margin-top: 12px;
`
const MainMenus = styled.div`
display: flex;

View File

@@ -0,0 +1,2 @@
export const DEFAULT_TEMPERATURE = 0.7
export const DEFAULT_CONEXTCOUNT = 5

View File

@@ -320,6 +320,22 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
enabled: true
}
],
aihubmix: [
{
id: 'gpt-4o-mini',
provider: 'aihubmix',
name: 'GPT-4o Mini',
group: 'GPT-4o',
enabled: true
},
{
id: 'aihubmix-Llama-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama 3 70B Instruct',
group: 'Llama3',
enabled: true
}
],
openrouter: [
{
id: 'google/gemma-2-9b-it:free',

View File

@@ -10,6 +10,7 @@ import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
@@ -49,6 +50,8 @@ export function getProviderLogo(providerId: string) {
return DashScopeProviderLogo
case 'anthropic':
return AnthropicProviderLogo
case 'aihubmix':
return AiHubMixProviderLogo
default:
return undefined
}
@@ -175,5 +178,13 @@ export const PROVIDER_CONFIG = {
docs: 'https://docs.anthropic.com/en/docs',
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
}
},
aihubmix: {
websites: {
official: 'https://aihubmix.com/',
apiKey: 'https://aihubmix.com/token',
docs: 'https://doc.aihubmix.com/',
models: 'https://aihubmix.com/models'
}
}
}

View File

@@ -3,17 +3,28 @@ import {
addModel as _addModel,
removeModel as _removeModel,
updateProvider as _updateProvider,
updateProviders as _updateProviders
updateProviders as _updateProviders,
addProvider,
removeProvider
} from '@renderer/store/llm'
import { Assistant, Model, Provider } from '@renderer/types'
import { useDefaultModel } from './useAssistant'
import { createSelector } from '@reduxjs/toolkit'
const selectEnabledProviders = createSelector(
(state) => state.llm.providers,
(providers) => providers.filter((p) => p.enabled)
)
export function useProviders() {
const providers = useAppSelector((state) => state.llm.providers.filter((p) => p.enabled))
const providers = useAppSelector(selectEnabledProviders)
const dispatch = useAppDispatch()
return {
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))
}
}
@@ -22,6 +33,14 @@ export function useSystemProviders() {
return useAppSelector((state) => state.llm.providers.filter((p) => p.isSystem))
}
export function useUserProviders() {
return useAppSelector((state) => state.llm.providers.filter((p) => !p.isSystem))
}
export function useAllProviders() {
return useAppSelector((state) => state.llm.providers)
}
export function useProvider(id: string) {
const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === id) as Provider)
const dispatch = useAppDispatch()

View File

@@ -44,7 +44,7 @@ const resources = {
'chat.completion.paused': 'Chat completion paused'
},
assistant: {
'default.name': 'Default Assistant',
'default.name': '😀 Default Assistant',
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
'default.topic.name': 'Default Topic',
'topics.title': 'Topics',
@@ -64,7 +64,17 @@ const resources = {
'input.clear.content': 'Are you sure to clear all messages?',
'input.placeholder': 'Type your message here...',
'input.send': 'Send',
'input.pause': 'Pause'
'input.pause': 'Pause',
'input.settings': 'Settings',
'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.',
'settings.conext_count': 'Context',
'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
'settings.reset': 'Reset',
'settings.set_as_default': 'Apply to default assistant',
'settings.max': 'Max'
},
apps: {
title: 'Agents'
@@ -81,7 +91,8 @@ const resources = {
ollama: 'Ollama',
baichuan: 'Baichuan',
dashscope: 'DashScope',
anthropic: 'Anthropic'
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
},
settings: {
title: 'Settings',
@@ -101,7 +112,6 @@ const resources = {
'models.default_assistant_model': 'Default Assistant Model',
'models.topic_naming_model': 'Topic Naming Model',
'models.add.add_model': 'Add Model',
'models.add.provider_name.placeholder': 'Provider Name',
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
'models.add.model_id': 'Model ID',
'models.add.model_id.tooltip': 'Example: gpt-3.5-turbo',
@@ -112,12 +122,17 @@ const resources = {
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
'models.empty': 'No models found',
'assistant.title': 'Default Assistant',
'assistant.model_params': 'Model Parameters',
'about.description': 'A powerful AI assistant for producer',
'about.updateNotAvailable': 'You are using the latest version',
'about.checkingUpdate': 'Checking for updates...',
'about.updateError': 'Update error',
'about.checkUpdate': 'Check Update',
'about.downloading': 'Downloading...'
'about.downloading': 'Downloading...',
'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'
}
}
},
@@ -162,7 +177,7 @@ const resources = {
'chat.completion.paused': '会话已停止'
},
assistant: {
'default.name': '默认助手',
'default.name': '😃 默认助手 - Assistant',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题',
'topics.title': '话题',
@@ -182,7 +197,18 @@ const resources = {
'input.clear.content': '确定要清除所有消息吗?',
'input.placeholder': '在这里输入消息...',
'input.send': '发送',
'input.pause': '暂停'
'input.pause': '暂停',
'input.settings': '设置',
'input.estimated_tokens': '预估消耗',
'settings.temperature': '模型温度',
'settings.temperature.tip':
'模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7',
'settings.conext_count': '上下文数',
'settings.conext_count.tip':
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10',
'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手',
'settings.max': '不限'
},
apps: {
title: '智能体'
@@ -199,11 +225,12 @@ const resources = {
ollama: 'Ollama',
baichuan: '百川',
dashscope: '阿里云灵积',
anthropic: 'Anthropic'
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
},
settings: {
title: '设置',
general: '常规',
general: '常规设置',
provider: '模型提供商',
model: '模型设置',
assistant: '默认助手',
@@ -219,7 +246,6 @@ const resources = {
'models.default_assistant_model': '默认助手模型',
'models.topic_naming_model': '话题命名模型',
'models.add.add_model': '添加模型',
'models.add.provider_name.placeholder': '必填 例如 OpenAI',
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
'models.add.model_id': '模型 ID',
'models.add.model_id.tooltip': '例如 gpt-3.5-turbo',
@@ -230,12 +256,17 @@ const resources = {
'models.add.group_name.placeholder': '例如 ChatGPT',
'models.empty': '没有模型',
'assistant.title': '默认助手',
'assistant.model_params': '模型参数',
'about.description': '一个为创造者而生的 AI 助手',
'about.updateNotAvailable': '你的软件已是最新版本',
'about.checkingUpdate': '正在检查更新...',
'about.updateError': '更新出错',
'about.checkUpdate': '检查更新',
'about.downloading': '正在下载更新...'
'about.downloading': '正在下载更新...',
'provider.delete.title': '删除提供商',
'provider.delete.content': '确定要删除此模型提供商吗?',
'provider.edit.name': '模型提供商名称',
'provider.edit.name.placeholder': '例如 OpenAI'
}
}
}

View File

@@ -0,0 +1,180 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Button, Col, InputNumber, Popover, Row, Slider, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { FC, PropsWithChildren, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
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 { t } = useTranslation()
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
}
})
},
1000,
{ leading: false, trailing: true }
),
[]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ _temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ _contextCount: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT
}
})
}
useEffect(() => {
setTemperature(assistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
}, [assistant])
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={6}>
<Row align="middle" justify="end">
<Label>{t('assistant.settings.temperature')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
</Col>
<Col span={14}>
<Slider
min={0}
max={1.2}
onChange={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }}
step={0.1}
/>
</Col>
<Col span={3}>
<InputNumber
min={0}
max={1.2}
style={{ width: 50, marginLeft: 5, textAlign: 'center' }}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
controls={false}
/>
</Col>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={6}>
<Row align="middle" justify="end">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
</Col>
<Col span={14}>
<Slider
min={0}
max={20}
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('assistant.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
<Col span={3}>
<InputNumber
min={0}
max={20}
style={{ width: 50, marginLeft: 5, textAlign: 'center' }}
step={1}
value={contextCount}
onChange={onConextCountChange}
controls={false}
/>
</Col>
</Row>
<Row justify="center">
<Button onClick={onReset}>{t('assistant.settings.reset')}</Button>
</Row>
</Container>
)
}
const AssistantSettings: FC<Props & PropsWithChildren> = ({ children, assistant }) => {
const [open, setOpen] = useState(false)
const { t } = useTranslation()
return (
<Popover content={<PopoverContent assistant={assistant} />} trigger="click" onOpenChange={setOpen}>
{open ? (
children
) : (
<Tooltip placement="top" title={t('assistant.input.settings')} arrow>
{children}
</Tooltip>
)}
</Popover>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 8px;
width: 420px;
padding: 5px;
`
const Label = styled.p`
margin: 0;
font-size: 14px;
font-weight: bold;
margin-right: 5px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 14px;
cursor: pointer;
color: var(--color-text-3);
`
export default AssistantSettings

View File

@@ -24,11 +24,9 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
const { t } = useTranslation()
const onDelete = (assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
removeAssistant(assistant.id)
setTimeout(() => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
}, 0)
}
const items: MenuProps['items'] = [

View File

@@ -16,10 +16,6 @@ const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
if (!assistant) {
return null
}
return (
<Container id="chat">
<Flex vertical flex={1} justify="space-between">

View File

@@ -1,7 +1,7 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
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'
@@ -9,6 +9,7 @@ import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { useAssistant } from '@renderer/hooks/useAssistant'
import {
ClearOutlined,
ControlOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
@@ -16,7 +17,7 @@ import {
PlusCircleOutlined
} from '@ant-design/icons'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import { isEmpty } from 'lodash'
import { debounce, isEmpty } from 'lodash'
import SendMessageSetting from './SendMessageSetting'
import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
@@ -24,6 +25,7 @@ import store, { useAppSelector } from '@renderer/store'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useTranslation } from 'react-i18next'
import { setGenerating } from '@renderer/store/runtime'
import AssistantSettings from './AssistantSettings'
interface Props {
assistant: Assistant
@@ -36,6 +38,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut } = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const generating = useAppSelector((state) => state.runtime.generating)
const inputRef = useRef<TextAreaRef>(null)
@@ -65,6 +68,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
setText('')
}
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
if (event.shiftKey) {
@@ -108,11 +113,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [addNewTopic, generating])
useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
setText(message.content)
inputRef.current?.focus()
})
}),
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [])
@@ -148,6 +155,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton>
</Popconfirm>
</Tooltip>
<AssistantSettings assistant={assistant}>
<ToolbarButton type="text">
<ControlOutlined />
</ToolbarButton>
</AssistantSettings>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
@@ -177,9 +189,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
autoFocus
contextMenu="true"
variant="borderless"
styles={{ textarea: { paddingLeft: 0 } }}
showCount
ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }}
/>
<TextCount>
{t('assistant.input.estimated_tokens')}: {`${inputTokenCount}/${estimateTokenCount}`}
</TextCount>
</Container>
)
}
@@ -192,6 +208,7 @@ const Container = styled.div`
border-top: 0.5px solid var(--color-border);
padding: 5px 15px;
transition: all 0.3s ease;
position: relative;
`
const Textarea = styled(TextArea)`
@@ -235,4 +252,12 @@ const ToolbarButton = styled(Button)`
}
`
const TextCount = styled.div`
position: absolute;
right: 8px;
bottom: 8px;
font-size: 11px;
color: var(--color-text-3);
`
export default Inputbar

View File

@@ -104,8 +104,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
<MessageMetadata>{message.modelId}</MessageMetadata>
{message.usage && (
<>
<MessageMetadata style={{ textTransform: 'uppercase' }}>
tokens used: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
<MessageMetadata>
Tokens: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
{message.usage.completion_tokens})
</MessageMetadata>
</>

View File

@@ -7,7 +7,7 @@ import MessageItem from './Message'
import { reverse } from 'lodash'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { runAsyncFunction } from '@renderer/utils'
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { t } from 'i18next'
@@ -22,7 +22,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { updateTopic } = useAssistant(assistant.id)
const provider = useProviderByAssistant(assistant)
const messagesRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const assistantDefaultMessage: Message = {
id: 'assistant',
@@ -95,11 +95,15 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
}, [topic.id])
useEffect(() => {
messagesRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
}, [messages])
useEffect(() => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
}, [assistant, messages])
return (
<Container id="messages" key={assistant.id} ref={messagesRef}>
<Container id="messages" key={assistant.id} ref={containerRef}>
{lastMessage && <MessageItem message={lastMessage} />}
{reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />

View File

@@ -22,7 +22,7 @@ const Navigation: FC<Props> = ({ activeAssistant }) => {
.filter((p) => p.models.length > 0)
.map((p) => ({
key: p.id,
label: t(`provider.${p.id}`),
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: p.models.map((m) => ({
key: m.id,

View File

@@ -28,7 +28,7 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
return (
<Dropdown
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
placement="top"
placement="topRight"
trigger={['click']}
arrow>
{children}

View File

@@ -82,12 +82,8 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
}
}
if (!showRightSidebar) {
return null
}
return (
<Container className={showRightSidebar ? '' : 'collapsed'}>
<Container style={{ display: showRightSidebar ? 'block' : 'none' }}>
<TopicTitle>
<span>
{t('assistant.topics.title')} ({assistant.topics.length})

View File

@@ -1,15 +1,66 @@
import { FC } from 'react'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
import { Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { QuestionCircleOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { Button, Col, Input, InputNumber, Row, Slider, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
import { debounce } from 'lodash'
const AssistantSettings: FC = () => {
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()
const [temperature, setTemperature] = useState(defaultAssistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const { t } = useTranslation()
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
updateDefaultAssistant({
...defaultAssistant,
settings: {
...defaultAssistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
}
})
},
1000,
{ leading: false, trailing: true }
),
[]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ _temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ _contextCount: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateDefaultAssistant({
...defaultAssistant,
settings: {
...defaultAssistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT
}
})
}
return (
<SettingContainer>
<SettingTitle>{t('settings.assistant.title')}</SettingTitle>
@@ -27,8 +78,82 @@ const AssistantSettings: FC = () => {
value={defaultAssistant.prompt}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
/>
<SettingDivider />
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.assistant.model_params')}</SettingSubtitle>
<Row align="middle">
<Label>{t('assistant.settings.temperature')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={22}>
<Slider
min={0}
max={1.2}
onChange={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }}
step={0.1}
/>
</Col>
<Col span={2}>
<InputNumber
min={0}
max={1.2}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={22}>
<Slider
min={0}
max={20}
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('assistant.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
<Col span={2}>
<InputNumber
min={0}
max={20}
step={1}
value={contextCount}
onChange={onConextCountChange}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Button onClick={onReset} style={{ width: 100 }}>
{t('assistant.settings.reset')}
</Button>
</SettingContainer>
)
}
const Label = styled.p`
margin: 0;
font-size: 14px;
font-weight: bold;
margin-right: 5px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 14px;
cursor: pointer;
color: var(--color-text-3);
`
export default AssistantSettings

View File

@@ -10,7 +10,7 @@ import { setAvatar } from '@renderer/store/runtime'
import { useSettings } from '@renderer/hooks/useSettings'
import { setLanguage } from '@renderer/store/settings'
import { useTranslation } from 'react-i18next'
import i18next from 'i18next'
import i18n from '@renderer/i18n'
const GeneralSettings: FC = () => {
const avatar = useAvatar()
@@ -20,7 +20,7 @@ const GeneralSettings: FC = () => {
const onSelectLanguage = (value: string) => {
dispatch(setLanguage(value))
i18next.changeLanguage(value)
i18n.changeLanguage(value)
localStorage.setItem('language', value)
}

View File

@@ -16,7 +16,7 @@ const ModelSettings: FC = () => {
const selectOptions = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
label: t(`provider.${p.id}`),
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
title: p.name,
options: p.models.map((m) => ({
label: m.name,

View File

@@ -1,21 +1,26 @@
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { useProviders, useSystemProviders } from '@renderer/hooks/useProvider'
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
import { getProviderLogo } from '@renderer/config/provider'
import { Provider } from '@renderer/types'
import { droppableReorder } from '@renderer/utils'
import { Avatar, Tag } from 'antd'
import { droppableReorder, generateColorFromChar, getFirstCharacter, uuid } from '@renderer/utils'
import { Avatar, Button, Dropdown, MenuProps, Tag } from 'antd'
import { FC, useState } from 'react'
import styled from 'styled-components'
import ProviderSetting from './components/ProviderSetting'
import { useTranslation } from 'react-i18next'
import { PlusOutlined } from '@ant-design/icons'
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
import AddProviderPopup from './components/AddProviderPopup'
const ProviderSettings: FC = () => {
const providers = useSystemProviders()
const { updateProviders } = useProviders()
const providers = useAllProviders()
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
const { t } = useTranslation()
const [dragging, setDragging] = useState(false)
const onDragEnd = (result: DropResult) => {
setDragging(false)
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
@@ -24,37 +29,109 @@ const ProviderSettings: FC = () => {
}
}
const onAddProvider = async () => {
const prividerName = await AddProviderPopup.show()
if (!prividerName) {
return
}
const provider = {
id: uuid(),
name: prividerName,
apiKey: '',
apiHost: '',
models: [],
enabled: false,
isSystem: false
} as Provider
addProvider(provider)
setSelectedProvider(provider)
}
const getDropdownMenus = (provider: Provider): MenuProps['items'] => {
return [
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
const name = await AddProviderPopup.show(provider)
name && updateProvider({ ...provider, name })
}
},
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
async onClick() {
window.modal.confirm({
title: t('settings.provider.delete.title'),
content: t('settings.provider.delete.content'),
okButtonProps: { danger: true },
okText: t('common.delete'),
onOk: () => {
setSelectedProvider(providers.filter((p) => p.isSystem)[0])
removeProvider(provider)
}
})
}
}
]
}
return (
<Container>
<ProviderListContainer>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{providers.map((provider, index) => (
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<ProviderListItem
key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
<Avatar src={getProviderLogo(provider.id)} size={24} />
<ProviderItemName>{t(`provider.${provider.id}`)}</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto' }}>
ON
</Tag>
)}
</ProviderListItem>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
<ProviderList>
<DragDropContext onDragStart={() => setDragging(true)} onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{providers.map((provider, index) => (
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown
menu={{ items: provider.isSystem ? [] : getDropdownMenus(provider) }}
trigger={['contextMenu']}>
<ProviderListItem
key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={28} />}
{!provider.isSystem && (
<Avatar
size={28}
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 28 }}>
{getFirstCharacter(provider.name)}
</Avatar>
)}
<ProviderItemName>
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto' }}>
ON
</Tag>
)}
</ProviderListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</ProviderList>
{!dragging && (
<AddButtonWrapper>
<Button type="dashed" style={{ width: '100%' }} icon={<PlusOutlined />} onClick={onAddProvider} />
</AddButtonWrapper>
)}
</ProviderListContainer>
<ProviderSetting provider={selectedProvider} key={JSON.stringify(selectedProvider)} />
</Container>
@@ -65,22 +142,30 @@ const Container = styled.div`
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
`
const ProviderListContainer = styled.div`
display: flex;
flex-direction: column;
width: var(--assistants-width);
height: 100%;
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
padding: 10px;
padding: 10px 8px;
overflow-y: auto;
`
const ProviderList = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`
const ProviderListItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 10px;
padding: 5px 8px;
margin-bottom: 5px;
width: 100%;
cursor: pointer;
@@ -99,6 +184,17 @@ const ProviderListItem = styled.div`
const ProviderItemName = styled.div`
margin-left: 10px;
font-weight: bold;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`
const AddButtonWrapper = styled.div`
height: 50px;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10px 0;
`
export default ProviderSettings

View File

@@ -48,8 +48,8 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
}
const model: Model = {
id: values.id,
provider: provider.id,
id: values.id,
name: values.name ? values.name : values.id.toUpperCase(),
group: getDefaultGroupName(values.group || values.id)
}
@@ -75,9 +75,6 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
colon={false}
style={{ marginTop: 25 }}
onFinish={onFinish}>
<Form.Item name="provider" label={t('common.provider')} initialValue={provider.id} rules={[{ required: true }]}>
<Input placeholder={t('settings.models.add.provider_name.placeholder')} disabled />
</Form.Item>
<Form.Item
name="id"
label={t('settings.models.add.model_id')}
@@ -86,13 +83,17 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
<Input
placeholder={t('settings.models.add.model_id.placeholder')}
spellCheck={false}
maxLength={50}
onChange={(e) => {
form.setFieldValue('name', e.target.value.toUpperCase())
form.setFieldValue('group', getDefaultGroupName(e.target.value))
}}
/>
</Form.Item>
<Form.Item name="name" label={t('settings.models.add.model_name')} tooltip="Example: GPT-3.5">
<Form.Item
name="name"
label={t('settings.models.add.model_name')}
tooltip={t('settings.models.add.model_name.placeholder')}>
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item

View File

@@ -0,0 +1,72 @@
import { TopView } from '@renderer/components/TopView'
import { Provider } from '@renderer/types'
import { Input, Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
provider?: Provider
resolve: (name: string) => void
}
const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const [open, setOpen] = useState(true)
const [name, setName] = useState(provider?.name || '')
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
resolve(name)
}
const onCancel = () => {
setOpen(false)
resolve('')
}
const onClose = () => {
resolve(name)
}
const buttonDisabled = name.length === 0
return (
<Modal
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
width={360}
closable={false}
title={t('settings.provider.edit.name')}
okButtonProps={{ disabled: buttonDisabled }}>
<Input
value={name}
onChange={(e) => setName(e.target.value.trim())}
placeholder={t('settings.provider.edit.name.placeholder')}
onKeyDown={(e) => e.key === 'Enter' && onOk()}
maxLength={32}
/>
</Modal>
)
}
export default class AddProviderPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
}
static show(provider?: Provider) {
return new Promise<string>((resolve) => {
this.topviewId = TopView.show(
<PopupContainer
provider={provider}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
)
})
}
}

View File

@@ -86,7 +86,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
return (
<Flex>
<ModelHeaderTitle>
{t(`provider.${provider.id}`)} {t('common.models')}
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name} {t('common.models')}
</ModelHeaderTitle>
{loading && <LoadingOutlined size={20} />}
</Flex>

View File

@@ -37,7 +37,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const onUpdateApiKey = () => updateProvider({ ...provider, apiKey })
const onUpdateApiHost = () => updateProvider({ ...provider, apiHost })
const onManageModel = () => EditModelsPopup.show({ provider })
const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add_model'), provider })
const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add.add_model'), provider })
const onCheckApi = async () => {
setApiChecking(true)
@@ -59,7 +59,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<SettingContainer>
<SettingTitle>
<Flex align="center">
<span>{t(`provider.${provider.id}`)}</span>
<span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span>
{officialWebsite! && (
<Link target="_blank" href={providerConfig.websites.official}>
<ExportOutlined style={{ marginLeft: '8px', color: 'white', fontSize: '12px' }} />

View File

@@ -6,7 +6,7 @@ import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } fr
import { sum, takeRight } from 'lodash'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { EVENT_NAMES } from './event'
import { removeQuotes } from '@renderer/utils'
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
export default class ProviderSDK {
provider: Provider
@@ -32,10 +32,11 @@ export default class ProviderSDK {
) {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const { contextCount } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
const userMessages = takeRight(messages, 5).map((message) => ({
const userMessages = takeRight(messages, contextCount + 1).map((message) => ({
role: message.role,
content: message.content
}))
@@ -43,9 +44,10 @@ export default class ProviderSDK {
if (this.isAnthropic) {
await this.anthropicSdk.messages
.stream({
max_tokens: 2048,
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
model: model.id
max_tokens: 4096,
temperature: assistant.settings?.temperature
})
.on('text', (text) => onChunk({ text: text || '' }))
.on('finalMessage', (message) =>
@@ -61,7 +63,8 @@ export default class ProviderSDK {
const stream = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true
stream: true,
temperature: assistant.settings?.temperature
})
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break

View File

@@ -10,5 +10,6 @@ export const EVENT_NAMES = {
ADD_ASSISTANT: 'ADD_ASSISTANT',
EDIT_MESSAGE: 'EDIT_MESSAGE',
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE',
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED'
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED',
ESTIMATED_TOKEN_COUNT: 'ESTIMATED_TOKEN_COUNT'
}

View File

@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 11,
version: 13,
blacklist: ['runtime'],
migrate
},

View File

@@ -94,6 +94,15 @@ const initialState: LlmState = {
isSystem: true,
enabled: false
},
{
id: 'aihubmix',
name: 'AiHubMix',
apiKey: '',
apiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',
@@ -137,8 +146,8 @@ const settingsSlice = createSlice({
addProvider: (state, action: PayloadAction<Provider>) => {
state.providers.push(action.payload)
},
removeProvider: (state, action: PayloadAction<{ id: string }>) => {
state.providers = state.providers.filter((p) => p.id !== action.payload.id && !p.isSystem)
removeProvider: (state, action: PayloadAction<Provider>) => {
state.providers = state.providers.filter((p) => p.id !== action.payload.id)
},
addModel: (state, action: PayloadAction<{ providerId: string; model: Model }>) => {
state.providers = state.providers.map((p) =>

View File

@@ -207,6 +207,42 @@ const migrate = createMigrate({
]
}
}
},
// @ts-ignore store type is unknown
'12': (state: RootState) => {
return {
...state,
llm: {
...state.llm,
providers: [
...state.llm.providers,
{
id: 'aihubmix',
name: 'AiHubMix',
apiKey: '',
apiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
isSystem: true,
enabled: false
}
]
}
}
},
// @ts-ignore store type is unknown
'13': (state: RootState) => {
return {
...state,
assistants: {
...state.assistants,
defaultAssistant: {
...state.assistants.defaultAssistant,
name: ['Default Assistant', '默认助手'].includes(state.assistants.defaultAssistant.name)
? i18n.t(`assistant.default.name`)
: state.assistants.defaultAssistant.name
}
}
}
}
})

View File

@@ -7,6 +7,12 @@ export type Assistant = {
prompt: string
topics: Topic[]
model?: Model
settings?: AssistantSettings
}
export type AssistantSettings = {
contextCount: number
temperature: number
}
export type Message = {

View File

@@ -1,6 +1,9 @@
import { v4 as uuidv4 } from 'uuid'
import imageCompression from 'browser-image-compression'
import { Model } from '@renderer/types'
import { Assistant, AssistantSettings, Message, Model } from '@renderer/types'
import { GPTTokens } from 'gpt-tokens'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { takeRight } from 'lodash'
export const runAsyncFunction = async (fn: () => void) => {
await fn()
@@ -132,3 +135,66 @@ export function getErrorMessage(error: any) {
export function removeQuotes(str) {
return str.replace(/['"]+/g, '')
}
export function generateColorFromChar(char) {
// 使用字符的Unicode值作为随机种子
const seed = char.charCodeAt(0)
// 使用简单的线性同余生成器创建伪随机数
const a = 1664525
const c = 1013904223
const m = Math.pow(2, 32)
// 生成三个伪随机数作为RGB值
let r = (a * seed + c) % m
let g = (a * r + c) % m
let b = (a * g + c) % m
// 将伪随机数转换为0-255范围内的整数
r = Math.floor((r / m) * 256)
g = Math.floor((g / m) * 256)
b = Math.floor((b / m) * 256)
// 返回十六进制颜色字符串
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
export function getFirstCharacter(str) {
if (str.length === 0) return ''
// 使用 for...of 循环来获取第一个字符
for (const char of str) {
return char
}
}
export const getAssistantSettings = (assistant: Assistant): AssistantSettings => {
const contextCount = assistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
return {
contextCount: contextCount === 20 ? 100000 : contextCount,
temperature: assistant.settings?.temperature ?? DEFAULT_TEMPERATURE
}
}
export function estimateInputTokenCount(text: string) {
const input = new GPTTokens({
model: 'gpt-4o',
messages: [{ role: 'user', content: text }]
})
return input.usedTokens - 7
}
export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[]) {
const { contextCount } = getAssistantSettings(assistant)
const all = new GPTTokens({
model: 'gpt-4o',
messages: [
{ role: 'system', content: assistant.prompt },
...takeRight(msgs, contextCount).map((message) => ({ role: message.role, content: message.content }))
]
})
return all.usedTokens - 7
}

View File

@@ -117,7 +117,7 @@
<p class="description">Windows/macOS GPT 客户端</p>
<div class="download-buttons">
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.7/Cherry-Studio-0.2.7-x64.dmg"
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-x64.dmg"
class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24">
<path
@@ -127,7 +127,7 @@
macOS Intel
</a>
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.7/Cherry-Studio-0.2.7-arm64.dmg"
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-arm64.dmg"
class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24">
<path
@@ -137,7 +137,7 @@
macOS Apple Silicon
</a>
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.7/Cherry-Studio-0.2.7-setup.exe"
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-setup.exe"
class="download-btn">
<svg viewBox="0 0 448 512" width="24" height="24">
<path
@@ -149,7 +149,7 @@
</div>
<p class="new-app">
🎉 <a href="https://github.com/kangfenmao/cherry-studio" target="_blank">Cherry Studio AI</a> 最新版本
<a href="https://github.com/kangfenmao/cherry-studio/releases/tag/v0.2.7" target="_blank">v0.2.7</a> 发布啦!
<a href="https://github.com/kangfenmao/cherry-studio/releases/tag/v0.3.0" target="_blank">v0.3.0</a> 发布啦!
</p>
<div class="footer">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">开源</a> |

View File

@@ -3440,6 +3440,7 @@ __metadata:
eslint-plugin-react: "npm:^7.34.3"
eslint-plugin-react-hooks: "npm:^4.6.2"
eslint-plugin-unused-imports: "npm:^4.0.0"
gpt-tokens: "npm:^1.3.6"
i18next: "npm:^23.11.5"
localforage: "npm:^1.10.0"
lodash: "npm:^4.17.21"
@@ -3797,6 +3798,13 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.4.3
resolution: "decimal.js@npm:10.4.3"
checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee
languageName: node
linkType: hard
"decode-named-character-reference@npm:^1.0.0":
version: 1.0.2
resolution: "decode-named-character-reference@npm:1.0.2"
@@ -5241,6 +5249,17 @@ __metadata:
languageName: node
linkType: hard
"gpt-tokens@npm:^1.3.6":
version: 1.3.6
resolution: "gpt-tokens@npm:1.3.6"
dependencies:
decimal.js: "npm:^10.4.3"
js-tiktoken: "npm:^1.0.10"
openai-chat-tokens: "npm:^0.2.8"
checksum: 10c0/0efc1da655a16a306df4f17646832693d7cbec569fe44d4fcc9d4a605f8614f1eb974e04b24a4e8c71095fe0fab6de7251a34c6e2d6805a5e1b5811eea37437b
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
@@ -6067,6 +6086,15 @@ __metadata:
languageName: node
linkType: hard
"js-tiktoken@npm:^1.0.10, js-tiktoken@npm:^1.0.7":
version: 1.0.12
resolution: "js-tiktoken@npm:1.0.12"
dependencies:
base64-js: "npm:^1.5.1"
checksum: 10c0/7afb4826e21342386a1884754fbc1c1828f948c4dd0ab093bf778d1323e65343bd5343d15f7cda46af396f1fe4a0297739936149b7c40a0601eefe3fcaef8727
languageName: node
linkType: hard
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
@@ -7194,6 +7222,15 @@ __metadata:
languageName: node
linkType: hard
"openai-chat-tokens@npm:^0.2.8":
version: 0.2.8
resolution: "openai-chat-tokens@npm:0.2.8"
dependencies:
js-tiktoken: "npm:^1.0.7"
checksum: 10c0/b415fda706b408f29b4584998990f29ad7f80f2ac1e84179a0976742ba8a80859fedeae5745a9bfe73443d95960b77328610074952ad198a18bc0e5c0ceb5b7b
languageName: node
linkType: hard
"openai@npm:^4.52.1":
version: 4.52.1
resolution: "openai@npm:4.52.1"