Compare commits

...

21 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
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
40 changed files with 858 additions and 455 deletions

View File

@@ -8,7 +8,7 @@
![](https://github.com/user-attachments/assets/18c10eed-4711-4975-bf9c-b274c61924f3) ![](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 # Feature

View File

@@ -56,4 +56,6 @@ electronDownload:
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
📢 新功能:可以添加自定义服务提供商了 支持设置模型 Temperature 参数
支持设置上下文数量
输入框增加 Token 消耗预估

View File

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

View File

@@ -1,45 +0,0 @@
# CHANGES LOG
### 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,46 +0,0 @@
# 更新日志
### 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 架构

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; --topic-list-width: 250px;
--settings-width: var(--assistants-width); --settings-width: var(--assistants-width);
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 120px; --input-bar-height: 125px;
} }
*, *,

View File

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

View File

@@ -44,14 +44,13 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 12px 0; padding: 8px 0;
min-width: var(--sidebar-width); min-width: var(--sidebar-width);
min-height: 100%; min-height: 100%;
-webkit-app-region: drag !important; -webkit-app-region: drag !important;
background-color: #1f1f1f; background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
margin-top: var(--navbar-height); padding-top: var(--navbar-height);
padding-bottom: calc(var(--navbar-height) + 6px);
` `
const AvatarImg = styled.img` const AvatarImg = styled.img`
@@ -60,6 +59,7 @@ const AvatarImg = styled.img`
height: 28px; height: 28px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin: 5px 0; margin: 5px 0;
margin-top: 12px;
` `
const MainMenus = styled.div` const MainMenus = styled.div`
display: flex; 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 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: [ openrouter: [
{ {
id: 'google/gemma-2-9b-it:free', 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 BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png' import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg' 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 ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg' import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png' import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
@@ -49,6 +50,8 @@ export function getProviderLogo(providerId: string) {
return DashScopeProviderLogo return DashScopeProviderLogo
case 'anthropic': case 'anthropic':
return AnthropicProviderLogo return AnthropicProviderLogo
case 'aihubmix':
return AiHubMixProviderLogo
default: default:
return undefined return undefined
} }
@@ -175,5 +178,13 @@ export const PROVIDER_CONFIG = {
docs: 'https://docs.anthropic.com/en/docs', docs: 'https://docs.anthropic.com/en/docs',
models: 'https://docs.anthropic.com/en/docs/about-claude/models' 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

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

View File

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

View File

@@ -1,25 +1,31 @@
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
addModel as _addModel, addModel,
removeModel as _removeModel,
updateProvider as _updateProvider,
updateProviders as _updateProviders,
addProvider, addProvider,
removeProvider removeModel,
removeProvider,
updateProvider,
updateProviders
} from '@renderer/store/llm' } from '@renderer/store/llm'
import { Assistant, Model, Provider } from '@renderer/types' import { Assistant, Model, Provider } from '@renderer/types'
import { useDefaultModel } from './useAssistant' 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() { export function useProviders() {
const providers = useAppSelector((state) => state.llm.providers.filter((p) => p.enabled)) const providers = useAppSelector(selectEnabledProviders)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return { return {
providers, providers,
addProvider: (provider: Provider) => dispatch(addProvider(provider)), addProvider: (provider: Provider) => dispatch(addProvider(provider)),
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)), removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)), updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
updateProviders: (providers: Provider[]) => dispatch(_updateProviders(providers)) updateProviders: (providers: Provider[]) => dispatch(updateProviders(providers))
} }
} }
@@ -42,9 +48,9 @@ export function useProvider(id: string) {
return { return {
provider, provider,
models: provider.models, models: provider.models,
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)), updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(_addModel({ providerId: id, model })), addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(_removeModel({ providerId: id, model })) removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
} }
} }

View File

@@ -41,10 +41,11 @@ const resources = {
'error.enter.model': 'Please select a model first', 'error.enter.model': 'Please select a model first',
'api.connection.failed': 'Connection failed', 'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful', '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: { 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.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',
@@ -64,7 +65,17 @@ const resources = {
'input.clear.content': 'Are you sure to clear all messages?', 'input.clear.content': 'Are you sure to clear all messages?',
'input.placeholder': 'Type your message here...', 'input.placeholder': 'Type your message here...',
'input.send': 'Send', '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: { apps: {
title: 'Agents' title: 'Agents'
@@ -81,15 +92,16 @@ const resources = {
ollama: 'Ollama', ollama: 'Ollama',
baichuan: 'Baichuan', baichuan: 'Baichuan',
dashscope: 'DashScope', dashscope: 'DashScope',
anthropic: 'Anthropic' anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
}, },
settings: { settings: {
title: 'Settings', title: 'Settings',
general: 'General', general: 'General Settings',
provider: 'Model Provider', provider: 'Model Provider',
model: 'Model Settings', model: 'Model Settings',
assistant: 'Default Assistant', assistant: 'Default Assistant',
about: 'About', about: 'About & Feedback',
'general.title': 'General Settings', 'general.title': 'General Settings',
'provider.api_key': 'API Key', 'provider.api_key': 'API Key',
'provider.check': 'Check', 'provider.check': 'Check',
@@ -111,6 +123,7 @@ const resources = {
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT', 'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
'models.empty': 'No models found', 'models.empty': 'No models found',
'assistant.title': 'Default Assistant', 'assistant.title': 'Default Assistant',
'assistant.model_params': 'Model Parameters',
'about.description': 'A powerful AI assistant for producer', 'about.description': 'A powerful AI assistant for producer',
'about.updateNotAvailable': 'You are using the latest version', 'about.updateNotAvailable': 'You are using the latest version',
'about.checkingUpdate': 'Checking for updates...', 'about.checkingUpdate': 'Checking for updates...',
@@ -120,7 +133,16 @@ const resources = {
'provider.delete.title': 'Delete Provider', 'provider.delete.title': 'Delete Provider',
'provider.delete.content': 'Are you sure you want to delete this provider?', 'provider.delete.content': 'Are you sure you want to delete this provider?',
'provider.edit.name': 'Provider Name', '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'
} }
} }
}, },
@@ -162,10 +184,11 @@ const resources = {
'error.enter.model': '请选择一个模型', 'error.enter.model': '请选择一个模型',
'api.connection.failed': '连接失败', 'api.connection.failed': '连接失败',
'api.connection.success': '连接成功', 'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止' 'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换'
}, },
assistant: { assistant: {
'default.name': '默认助手', 'default.name': '😃 默认助手 - Assistant',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。', 'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题', 'default.topic.name': '默认话题',
'topics.title': '话题', 'topics.title': '话题',
@@ -185,7 +208,18 @@ const resources = {
'input.clear.content': '确定要清除所有消息吗?', 'input.clear.content': '确定要清除所有消息吗?',
'input.placeholder': '在这里输入消息...', 'input.placeholder': '在这里输入消息...',
'input.send': '发送', '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: { apps: {
title: '智能体' title: '智能体'
@@ -202,15 +236,16 @@ const resources = {
ollama: 'Ollama', ollama: 'Ollama',
baichuan: '百川', baichuan: '百川',
dashscope: '阿里云灵积', dashscope: '阿里云灵积',
anthropic: 'Anthropic' anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
}, },
settings: { settings: {
title: '设置', title: '设置',
general: '常规', general: '常规设置',
provider: '模型提供商', provider: '模型提供商',
model: '模型设置', model: '模型设置',
assistant: '默认助手', assistant: '默认助手',
about: '关于', about: '关于我们',
'general.title': '常规设置', 'general.title': '常规设置',
'provider.api_key': 'API 密钥', 'provider.api_key': 'API 密钥',
'provider.check': '检查', 'provider.check': '检查',
@@ -232,7 +267,8 @@ const resources = {
'models.add.group_name.placeholder': '例如 ChatGPT', 'models.add.group_name.placeholder': '例如 ChatGPT',
'models.empty': '没有模型', 'models.empty': '没有模型',
'assistant.title': '默认助手', 'assistant.title': '默认助手',
'about.description': '一个为创造者而生的 AI 助手', 'assistant.model_params': '模型参数',
'about.description': '一款为创造者而生的 AI 助手',
'about.updateNotAvailable': '你的软件已是最新版本', 'about.updateNotAvailable': '你的软件已是最新版本',
'about.checkingUpdate': '正在检查更新...', 'about.checkingUpdate': '正在检查更新...',
'about.updateError': '更新出错', 'about.updateError': '更新出错',
@@ -241,7 +277,16 @@ const resources = {
'provider.delete.title': '删除提供商', 'provider.delete.title': '删除提供商',
'provider.delete.content': '确定要删除此模型提供商吗?', 'provider.delete.content': '确定要删除此模型提供商吗?',
'provider.edit.name': '模型提供商名称', '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

@@ -0,0 +1,180 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
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'
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> = (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 }) => {
updateAssistantSettings({
...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

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

View File

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

View File

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

View File

@@ -104,8 +104,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
<MessageMetadata>{message.modelId}</MessageMetadata> <MessageMetadata>{message.modelId}</MessageMetadata>
{message.usage && ( {message.usage && (
<> <>
<MessageMetadata style={{ textTransform: 'uppercase' }}> <MessageMetadata>
tokens used: {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}) {message.usage.completion_tokens})
</MessageMetadata> </MessageMetadata>
</> </>

View File

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

View File

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

View File

@@ -4,13 +4,14 @@ import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { fetchMessagesSummary } from '@renderer/services/api' import { fetchMessagesSummary } from '@renderer/services/api'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Button, Dropdown, MenuProps, Popconfirm } from 'antd' import { Button, Dropdown, MenuProps, Popconfirm } from 'antd'
import { FC, useRef } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons'
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd' import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils' import { droppableReorder } from '@renderer/utils'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useAppSelector } from '@renderer/store'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@@ -18,60 +19,61 @@ interface Props {
setActiveTopic: (topic: Topic) => void setActiveTopic: (topic: Topic) => void
} }
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => { const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar() const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id) const { assistant, removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(_assistant.id)
const currentTopic = useRef<Topic | null>(null)
const { t } = useTranslation() 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', label: t('assistant.topics.auto_rename'),
icon: <SignatureOutlined />, key: 'auto-rename',
async onClick() { icon: <SignatureOutlined />,
if (currentTopic.current) { async onClick() {
const messages = await LocalStorage.getTopicMessages(currentTopic.current.id) const messages = await LocalStorage.getTopicMessages(topic.id)
if (messages.length >= 2) { if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant }) const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) { if (summaryText) {
updateTopic({ ...currentTopic.current, name: summaryText }) updateTopic({ ...topic, name: summaryText })
} }
} }
} }
} },
}, {
{ label: t('common.rename'),
label: t('common.rename'), key: 'rename',
key: 'rename', icon: <EditOutlined />,
icon: <EditOutlined />, async onClick() {
async onClick() { const name = await PromptPopup.show({
const name = await PromptPopup.show({ title: t('assistant.topics.edit.title'),
title: t('assistant.topics.edit.title'), message: t('assistant.topics.edit.placeholder'),
message: t('assistant.topics.edit.placeholder'), defaultValue: topic?.name || ''
defaultValue: currentTopic.current?.name || '' })
}) if (name && topic?.name !== name) {
if (name && currentTopic.current && currentTopic.current?.name !== name) { updateTopic({ ...topic, name })
updateTopic({ ...currentTopic.current, name }) }
} }
} }
} ]
]
if (assistant.topics.length > 1) { if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' }) menus.push({ type: 'divider' })
topicMenuItems.push({ menus.push({
label: t('common.delete'), label: t('common.delete'),
danger: true, danger: true,
key: 'delete', key: 'delete',
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
onClick() { onClick() {
if (assistant.topics.length === 1) return if (assistant.topics.length === 1) return
currentTopic.current && removeTopic(currentTopic.current) removeTopic(topic)
currentTopic.current = null setActiveTopic(assistant.topics[0])
setActiveTopic(assistant.topics[0]) }
} })
}) }
return menus
} }
const onDragEnd = (result: DropResult) => { const onDragEnd = (result: DropResult) => {
@@ -82,12 +84,16 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
} }
} }
if (!showRightSidebar) { const onSwitchTopic = (topic: Topic) => {
return null if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
setActiveTopic(topic)
} }
return ( return (
<Container className={showRightSidebar ? '' : 'collapsed'}> <Container style={{ display: showRightSidebar ? 'block' : 'none' }}>
<TopicTitle> <TopicTitle>
<span> <span>
{t('assistant.topics.title')} ({assistant.topics.length}) {t('assistant.topics.title')} ({assistant.topics.length})
@@ -112,14 +118,10 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}> <Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
{(provided) => ( {(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}> <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown <Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
menu={{ items: topicMenuItems }}
trigger={['contextMenu']}
key={topic.id}
onOpenChange={(open) => open && (currentTopic.current = topic)}>
<TopicListItem <TopicListItem
className={topic.id === activeTopic?.id ? 'active' : ''} className={topic.id === activeTopic?.id ? 'active' : ''}
onClick={() => setActiveTopic(topic)}> onClick={() => onSwitchTopic(topic)}>
{topic.name} {topic.name}
</TopicListItem> </TopicListItem>
</Dropdown> </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 { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png' import Logo from '@renderer/assets/images/logo.png'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Changelog from './components/Changelog'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { ProgressInfo } from 'electron-updater' import { ProgressInfo } from 'electron-updater'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
const AboutSettings: FC = () => { const AboutSettings: FC = () => {
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
@@ -26,8 +26,17 @@ const AboutSettings: FC = () => {
{ leading: true, trailing: false } { leading: true, trailing: false }
) )
const onOpenWebsite = (suffix = '') => { const onOpenWebsite = (url: string) => {
window.api.openWebsite('https://github.com/kangfenmao/cherry-studio' + suffix) 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(() => { useEffect(() => {
@@ -69,57 +78,92 @@ const AboutSettings: FC = () => {
}, [t]) }, [t])
return ( return (
<Container> <SettingContainer>
<AvatarWrapper onClick={() => onOpenWebsite()}> <SettingTitle>{t('settings.about.title')}</SettingTitle>
{percent > 0 && ( <SettingDivider />
<ProgressCircle <AboutHeader>
type="circle" <Row align="middle">
size={104} <AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
percent={percent} {percent > 0 && (
showInfo={false} <ProgressCircle
strokeLinecap="butt" type="circle"
strokeColor="#67ad5b" size={84}
/> percent={percent}
)} showInfo={false}
<Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} /> strokeLinecap="butt"
</AvatarWrapper> strokeColor="#67ad5b"
<Title> />
Cherry Studio <Version onClick={() => onOpenWebsite('/releases')}>(v{version})</Version> )}
</Title> <Avatar src={Logo} size={80} style={{ minHeight: 80 }} />
<Description>{t('settings.about.description')}</Description> </AvatarWrapper>
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}> <VersionWrapper>
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')} <Title>Cherry Studio</Title>
</CheckUpdateButton> <Description>{t('settings.about.description')}</Description>
<Changelog /> <Tag
</Container> 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>
</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; display: flex;
flex: 1; flex-direction: row;
flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: space-between;
height: calc(100vh - var(--navbar-height)); width: 100%;
overflow-y: scroll; padding: 5px 0;
padding: 0; `
padding-bottom: 50px;
const VersionWrapper = styled.div`
display: flex;
flex-direction: column;
min-height: 80px;
justify-content: center;
align-items: flex-start;
` `
const Title = styled.div` const Title = styled.div`
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
margin: 10px 0; margin-bottom: 5px;
`
const Version = styled.span`
font-size: 14px;
color: var(--color-text-2);
margin: 10px 0;
text-align: center;
cursor: pointer;
` `
const Description = styled.div` const Description = styled.div`
@@ -128,18 +172,17 @@ const Description = styled.div`
text-align: center; text-align: center;
` `
const CheckUpdateButton = styled(Button)` const CheckUpdateButton = styled(Button)``
margin-top: 10px;
`
const AvatarWrapper = styled.div` const AvatarWrapper = styled.div`
position: relative; position: relative;
cursor: pointer; cursor: pointer;
margin-right: 15px;
` `
const ProgressCircle = styled(Progress)` const ProgressCircle = styled(Progress)`
position: absolute; position: absolute;
top: 48px; top: -2px;
left: -2px; left: -2px;
` `

View File

@@ -1,15 +1,66 @@
import { FC } from 'react' import { QuestionCircleOutlined } from '@ant-design/icons'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components' import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant' 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 { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
import { debounce } from 'lodash'
const AssistantSettings: FC = () => { const AssistantSettings: FC = () => {
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant() 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 { 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 ( return (
<SettingContainer> <SettingContainer>
<SettingTitle>{t('settings.assistant.title')}</SettingTitle> <SettingTitle>{t('settings.assistant.title')}</SettingTitle>
@@ -27,8 +78,82 @@ const AssistantSettings: FC = () => {
value={defaultAssistant.prompt} value={defaultAssistant.prompt}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })} 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> </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 export default AssistantSettings

View File

@@ -100,11 +100,11 @@ const ProviderSettings: FC = () => {
key={JSON.stringify(provider)} key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''} className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}> onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={24} />} {provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={28} />}
{!provider.isSystem && ( {!provider.isSystem && (
<Avatar <Avatar
size={24} size={28}
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 24 }}> style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 28 }}>
{getFirstCharacter(provider.name)} {getFirstCharacter(provider.name)}
</Avatar> </Avatar>
)} )}
@@ -151,7 +151,7 @@ const ProviderListContainer = styled.div`
width: var(--assistants-width); width: var(--assistants-width);
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
padding: 10px; padding: 10px 8px;
overflow-y: auto; overflow-y: auto;
` `
@@ -165,7 +165,7 @@ const ProviderListItem = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 6px 10px; padding: 5px 8px;
margin-bottom: 5px; margin-bottom: 5px;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;

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

@@ -6,7 +6,7 @@ import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } fr
import { sum, takeRight } from 'lodash' import { sum, takeRight } from 'lodash'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources' import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { EVENT_NAMES } from './event' import { EVENT_NAMES } from './event'
import { removeQuotes } from '@renderer/utils' import { getAssistantSettings, removeQuotes } from '@renderer/utils'
export default class ProviderSDK { export default class ProviderSDK {
provider: Provider provider: Provider
@@ -32,10 +32,11 @@ export default class ProviderSDK {
) { ) {
const defaultModel = getDefaultModel() const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel const model = assistant.model || defaultModel
const { contextCount } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined 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, role: message.role,
content: message.content content: message.content
})) }))
@@ -43,9 +44,10 @@ export default class ProviderSDK {
if (this.isAnthropic) { if (this.isAnthropic) {
await this.anthropicSdk.messages await this.anthropicSdk.messages
.stream({ .stream({
max_tokens: 2048, model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[], messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
model: model.id max_tokens: 4096,
temperature: assistant?.settings?.temperature
}) })
.on('text', (text) => onChunk({ text: text || '' })) .on('text', (text) => onChunk({ text: text || '' }))
.on('finalMessage', (message) => .on('finalMessage', (message) =>
@@ -61,7 +63,8 @@ export default class ProviderSDK {
const stream = await this.openaiSdk.chat.completions.create({ const stream = await this.openaiSdk.chat.completions.create({
model: model.id, model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[], messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true stream: true,
temperature: assistant?.settings?.temperature
}) })
for await (const chunk of stream) { for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
@@ -80,7 +83,7 @@ export default class ProviderSDK {
const systemMessage = { const systemMessage = {
role: 'system', role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要标点符号' content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
} }
if (this.isAnthropic) { if (this.isAnthropic) {

View File

@@ -10,5 +10,6 @@ export const EVENT_NAMES = {
ADD_ASSISTANT: 'ADD_ASSISTANT', ADD_ASSISTANT: 'ADD_ASSISTANT',
EDIT_MESSAGE: 'EDIT_MESSAGE', EDIT_MESSAGE: 'EDIT_MESSAGE',
REGENERATE_MESSAGE: 'REGENERATE_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

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

View File

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

View File

@@ -94,6 +94,15 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: false 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', id: 'openrouter',
name: 'OpenRouter', name: 'OpenRouter',

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 prompt: string
topics: Topic[] topics: Topic[]
model?: Model model?: Model
settings?: AssistantSettings
}
export type AssistantSettings = {
contextCount: number
temperature: number
} }
export type Message = { export type Message = {

View File

@@ -1,6 +1,9 @@
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import imageCompression from 'browser-image-compression' 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) => { export const runAsyncFunction = async (fn: () => void) => {
await fn() await fn()
@@ -164,3 +167,34 @@ export function getFirstCharacter(str) {
return char 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> <p class="description">Windows/macOS GPT 客户端</p>
<div class="download-buttons"> <div class="download-buttons">
<a <a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.8/Cherry-Studio-0.2.8-x64.dmg" href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-x64.dmg"
class="download-btn"> class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24"> <svg viewBox="0 0 384 512" width="24" height="24">
<path <path
@@ -127,7 +127,7 @@
macOS Intel macOS Intel
</a> </a>
<a <a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.8/Cherry-Studio-0.2.8-arm64.dmg" href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-arm64.dmg"
class="download-btn"> class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24"> <svg viewBox="0 0 384 512" width="24" height="24">
<path <path
@@ -137,7 +137,7 @@
macOS Apple Silicon macOS Apple Silicon
</a> </a>
<a <a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.8/Cherry-Studio-0.2.8-setup.exe" href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.3.0/Cherry-Studio-0.3.0-setup.exe"
class="download-btn"> class="download-btn">
<svg viewBox="0 0 448 512" width="24" height="24"> <svg viewBox="0 0 448 512" width="24" height="24">
<path <path
@@ -149,7 +149,7 @@
</div> </div>
<p class="new-app"> <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" target="_blank">Cherry Studio AI</a> 最新版本
<a href="https://github.com/kangfenmao/cherry-studio/releases/tag/v0.2.8" target="_blank">v0.2.8</a> 发布啦! <a href="https://github.com/kangfenmao/cherry-studio/releases/tag/v0.3.0" target="_blank">v0.3.0</a> 发布啦!
</p> </p>
<div class="footer"> <div class="footer">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">开源</a> | <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: "npm:^7.34.3"
eslint-plugin-react-hooks: "npm:^4.6.2" eslint-plugin-react-hooks: "npm:^4.6.2"
eslint-plugin-unused-imports: "npm:^4.0.0" eslint-plugin-unused-imports: "npm:^4.0.0"
gpt-tokens: "npm:^1.3.6"
i18next: "npm:^23.11.5" i18next: "npm:^23.11.5"
localforage: "npm:^1.10.0" localforage: "npm:^1.10.0"
lodash: "npm:^4.17.21" lodash: "npm:^4.17.21"
@@ -3797,6 +3798,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "decode-named-character-reference@npm:^1.0.0":
version: 1.0.2 version: 1.0.2
resolution: "decode-named-character-reference@npm:1.0.2" resolution: "decode-named-character-reference@npm:1.0.2"
@@ -5241,6 +5249,17 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.11 version: 4.2.11
resolution: "graceful-fs@npm:4.2.11" resolution: "graceful-fs@npm:4.2.11"
@@ -6067,6 +6086,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "js-tokens@npm:4.0.0" resolution: "js-tokens@npm:4.0.0"
@@ -7194,6 +7222,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "openai@npm:^4.52.1":
version: 4.52.1 version: 4.52.1
resolution: "openai@npm:4.52.1" resolution: "openai@npm:4.52.1"