feat: add i18n

This commit is contained in:
kangfenmao 2024-07-13 19:49:31 +08:00
parent 6c8d2b0f68
commit 4a0b394bf5
35 changed files with 512 additions and 232 deletions

View File

@ -37,11 +37,11 @@ function createWindow(): void {
mainWindow.webContents.on('context-menu', () => {
const menu = new Menu()
menu.append(new MenuItem({ label: 'Copy', role: 'copy' }))
menu.append(new MenuItem({ label: 'Paste', role: 'paste' }))
menu.append(new MenuItem({ label: 'Cut', role: 'cut' }))
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: 'Select All', role: 'selectAll' }))
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
menu.popup()
})
@ -68,7 +68,7 @@ function createWindow(): void {
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.

View File

@ -1,5 +1,5 @@
<!doctype html>
<html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Cherry Studio</title>

View File

@ -1,20 +1,20 @@
import '@fontsource/inter'
import store, { persistor } from '@renderer/store'
import { ConfigProvider } from 'antd'
import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import { AntdThemeConfig, getAntdLocale } from './config/antd'
import './i18n'
import AppsPage from './pages/apps/AppsPage'
import HomePage from './pages/home/HomePage'
import SettingsPage from './pages/settings/SettingsPage'
import { ConfigProvider } from 'antd'
import TopViewContainer from './components/TopView'
import { AntdThemeConfig } from './config/antd'
import './i18n'
function App(): JSX.Element {
return (
<ConfigProvider theme={AntdThemeConfig}>
<ConfigProvider theme={AntdThemeConfig} locale={getAntdLocale()}>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>

View File

@ -4,6 +4,7 @@ import { TopView } from '../TopView'
import { Box } from '../Layout'
import { Assistant } from '@renderer/types'
import TextArea from 'antd/es/input/TextArea'
import { useTranslation } from 'react-i18next'
interface AssistantSettingPopupShowParams {
assistant: Assistant
@ -18,6 +19,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
const [description, setDescription] = useState(assistant.description)
const [prompt, setPrompt] = useState(assistant.prompt)
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
@ -33,21 +35,30 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
return (
<Modal title={assistant.name} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}>
<Box mb={8}>Name</Box>
<Input placeholder="Assistant Name" value={name} onChange={(e) => setName(e.target.value)} />
<Box mb={8}>{t('common.name')}</Box>
<Input
placeholder={t('common.assistant') + t('common.name')}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Box mt={8} mb={8}>
Description
{t('common.description')}
</Box>
<TextArea
rows={2}
placeholder="Assistant Description"
placeholder={t('common.assistant') + t('common.description')}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<Box mt={8} mb={8}>
Prompt
{t('common.prompt')}
</Box>
<TextArea rows={4} placeholder="Assistant Prompt" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
<TextArea
rows={4}
placeholder={t('common.assistant') + t('common.prompt')}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</Modal>
)
}

View File

@ -1,4 +1,6 @@
import store from '@renderer/store'
import { theme, ThemeConfig } from 'antd'
import zhCN from 'antd/locale/zh_CN'
export const colorPrimary = '#00b96b'
@ -9,3 +11,16 @@ export const AntdThemeConfig: ThemeConfig = {
},
algorithm: [theme.darkAlgorithm]
}
export function getAntdLocale() {
const language = store.getState().settings.language
switch (language) {
case 'zh-CN':
return zhCN
case 'en-US':
return undefined
default:
return zhCN
}
}

View File

@ -1,31 +1,28 @@
import { SystemAssistant } from '@renderer/types'
export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
// Article
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D29',
name: '文章总结',
description: '自动总结文章内容,帮助读者从中获取更多的信息',
prompt: '总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复',
group: 'Article'
group: '文章'
},
// Writing
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
name: '论文',
description: '根据主题撰写内容翔实、有信服力的论文',
prompt:
'我希望你能作为一名学者行事。你将负责研究一个你选择的主题,并将研究结果以论文或文章的形式呈现出来。你的任务是确定可靠的来源,以结构良好的方式组织材料,并以引用的方式准确记录。',
group: 'Writing'
group: '写作'
},
// Translation
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D40',
name: '翻译成中文',
description: '你是一个好用的翻译助手, 可以把任何语言翻译成中文',
prompt:
'你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。',
group: 'Translation'
group: '翻译'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D41',
@ -33,16 +30,15 @@ export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
description: '你是一个好用的翻译助手, 可以把任何语言翻译成英文',
prompt:
'你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。',
group: 'Translation'
group: '翻译'
},
// Software Engineer
{
id: '43CEDACF-C9EB-431B-848C-4D08EC26EB90',
name: '软件工程师',
description: '高级软件工程师,可以解答各种技术问题',
prompt:
'你是一个高级软件工程师,你需要帮我解答各种技术难题、设计技术方案以及编写代码。你编写的代码必须可以正常运行,而且没有任何 Bug 和其他问题。',
group: 'Software Engineer'
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2A',
@ -50,7 +46,7 @@ export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
description: '高级前端工程师,可以解答各种技术问题',
prompt:
'你擅长使用 TypeScript, JavaScript, HMLT, CSS 等编程语言。同时你还会使用 Node.js 及各种包来解决开发中遇到的问题。你还会使用 React, Vue 等前端框架。对于我的问题希望你能给出具体的代码示例,最好能够封装成一个函数方便我复制运行测试。',
group: 'Software Engineer'
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2B',
@ -58,98 +54,97 @@ export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
description: '高级后端工程师,可以解答各种技术问题',
prompt:
'高级后端工程师技术难题解答服务器架构数据库优化API设计网络安全代码审查性能调优微服务分布式系统容器技术持续集成/持续部署(CI/CD)。',
group: 'Software Engineer'
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2D',
name: '测试工程师',
description: '高级测试工程师,可以解答各种测试相关问题',
prompt: '你是一个高级测试工程师,你需要帮我解答各种技术难题',
group: 'Software Engineer'
group: '软件工程师'
},
// Programming Languages Assistants
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2E',
name: 'Python 工程师',
description: '你是一个高级Python工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Python工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2F',
name: 'Java 工程师',
description: '你是一个高级Java工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Java工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
name: 'C# 工程师',
description: '你是一个高级C#工程师,你需要帮我解答各种技术难题',
prompt: '你是一个高级C#工程师,你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D31',
name: 'C++ 工程师',
description: '你是一个高级C++工程师,你需要帮我解答各种技术难题',
prompt: '你是一个高级C++工程师,你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D32',
name: 'C 工程师',
description: '你是一个高级C工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级C工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D33',
name: 'Go 工程师',
description: '你是一个高级Go工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Go工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D34',
name: 'Rust 工程师',
description: '你是一个高级Rust工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Rust工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D35',
name: 'PHP 工程师',
description: '你是一个高级PHP工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级PHP工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D36',
name: 'Ruby 工程师',
description: '你是一个高级Ruby工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Ruby工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D37',
name: 'Swift 工程师',
description: '你是一个高级Swift工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Swift工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D38',
name: 'Kotlin 工程师',
description: '你是一个高级Kotlin工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Kotlin工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D39',
name: 'Dart 工程师',
description: '你是一个高级Dart工程师你需要帮我解答各种技术难题',
prompt: '你是一个高级Dart工程师你需要帮我解答各种技术难题',
group: 'Programming Languages'
group: '编程语言'
}
]

View File

@ -1 +0,0 @@
export const DEFAULT_TOPIC_NAME = 'Default Topic'

View File

@ -0,0 +1,73 @@
export const PROVIDER_CONFIG = {
openai: {
websites: {
official: 'https://openai.com/',
apiKey: 'https://platform.openai.com/api-keys',
docs: 'https://platform.openai.com/docs',
models: 'https://platform.openai.com/docs/models'
}
},
silicon: {
websites: {
official: 'https://www.siliconflow.cn/',
apiKey: 'https://cloud.siliconflow.cn/account/ak',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'
}
},
deepseek: {
websites: {
official: 'https://deepseek.com/',
apiKey: 'https://platform.deepseek.com/api_keys',
docs: 'https://platform.deepseek.com/api-docs/',
models: 'https://platform.deepseek.com/api-docs/'
}
},
yi: {
websites: {
official: 'https://platform.lingyiwanwu.com/',
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
docs: 'https://platform.lingyiwanwu.com/docs',
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
}
},
zhipu: {
websites: {
official: 'https://open.bigmodel.cn/',
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
docs: 'https://open.bigmodel.cn/dev/howuse/introduction',
models: 'https://open.bigmodel.cn/modelcenter/square'
}
},
moonshot: {
websites: {
official: 'https://moonshot.ai/',
apiKey: 'https://platform.moonshot.cn/console/api-keys',
docs: 'https://platform.moonshot.cn/docs/',
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
}
},
openrouter: {
websites: {
official: 'https://openrouter.ai/',
apiKey: 'https://openrouter.ai/settings/keys',
docs: 'https://openrouter.ai/docs/quick-start',
models: 'https://openrouter.ai/docs/models'
}
},
groq: {
websites: {
official: 'https://groq.com/',
apiKey: 'https://console.groq.com/keys',
docs: 'https://console.groq.com/docs/quickstart',
models: 'https://console.groq.com/docs/models'
}
},
ollama: {
websites: {
official: 'https://ollama.com/',
docs: 'https://github.com/ollama/ollama/tree/main/docs',
models: 'https://ollama.com/library'
}
}
}

View File

@ -11,7 +11,6 @@ export function useAppInitEffect() {
runAsyncFunction(async () => {
const storedImage = await LocalStorage.getImage('avatar')
storedImage && dispatch(setAvatar(storedImage))
console.debug('Avatar loaded from storage')
})
}, [dispatch])
}

View File

@ -1,28 +1,217 @@
import store from '@renderer/store'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
const resources = {
'en-US': {
translation: {
common: {
avatar: 'Avatar',
language: 'Language',
model: 'Model',
models: 'Models',
topics: 'Topics',
docs: 'Docs',
and: 'and',
assistant: 'Assistant',
name: 'Name',
description: 'Description',
prompt: 'Prompt',
rename: 'Rename',
delete: 'Delete',
edit: 'Edit',
duplicate: 'Duplicate',
copy: 'Copy',
regenerate: 'Regenerate',
provider: 'Provider'
},
button: {
add: 'Add',
manage: 'Manage',
select_model: 'Select Model'
},
message: {
copied: 'Copied!',
'assistant.added.content': 'Assistant added successfully',
'message.delete.title': 'Delete Message',
'message.delete.content': 'Are you sure you want to delete this message?',
'error.enter.api.key': 'Please enter your API key first',
'error.enter.api.host': 'Please enter your API host first',
'error.enter.model': 'Please select a model first',
'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful'
},
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',
'topics.hide_topics': 'Hide Topics',
'topics.show_topics': 'Show Topics',
'topics.auto_rename': 'Auto Rename',
'topics.edit.title': 'Rename',
'topics.edit.placeholder': 'Enter new name',
'topics.delete.all.title': 'Delete all topics',
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
'input.new_chat': ' New Chat ',
'input.topics': ' Topics ',
'input.clear': 'Clear',
'input.expand': 'Expand',
'input.collapse': 'Collapse',
'input.clear.title': 'Clear all messages?',
'input.clear.content': 'Are you sure to clear all messages?',
'input.placeholder': 'Type your message here...',
'input.send': 'Send'
},
apps: {
title: 'Agents'
},
provider: {
openai: 'OpenAI',
deepseek: 'DeepSeek',
moonshot: 'Moonshot',
silicon: 'SiliconFlow',
openrouter: 'OpenRouter',
yi: 'Lingyiwanwu',
zhipu: 'BigModel',
groq: 'Groq',
ollama: 'Ollama'
},
settings: {
title: 'Settings',
general: 'General',
provider: 'Model Provider',
model: 'Model Settings',
assistant: 'Default Assistant',
about: 'About'
about: 'About',
'general.title': 'General Settings',
'provider.api_key': 'API Key',
'provider.check': 'Check',
'provider.get_api_key': 'Get API Key',
'provider.api_host': 'API Host',
'provider.docs_check': 'Check',
'provider.docs_more_details': 'for more details',
'provider.search_placeholder': 'Search model id or name',
'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',
'models.add.model_name': 'Model Name',
'models.add.model_name.placeholder': 'Optional e.g. GPT-4',
'models.add.group_name': 'Group Name',
'models.add.group_name.tooltip': 'Optional e.g. ChatGPT',
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
'assistant.title': 'Default Assistant',
'about.description': 'A powerful AI assistant for producer'
}
}
},
'zh-CN': {
translation: {
common: {
avatar: '头像',
language: '语言',
model: '模型',
models: '模型',
topics: '话题',
docs: '文档',
and: '和',
assistant: '智能体',
name: '名称',
description: '描述',
prompt: '提示词',
rename: '重命名',
delete: '删除',
edit: '编辑',
duplicate: '复制',
copy: '复制',
regenerate: '重新生成',
provider: '提供商'
},
button: {
add: '添加',
manage: '管理',
select_model: '选择模型'
},
message: {
copied: '已复制!',
'assistant.added.content': '智能体添加成功',
'message.delete.title': '删除消息',
'message.delete.content': '确定要删除此消息吗?',
'error.enter.api.key': '请输入您的 API 密钥',
'error.enter.api.host': '请输入您的 API 地址',
'error.enter.model': '请选择一个模型',
'api.connection.failed': '连接失败',
'api.connection.successful': '连接成功'
},
assistant: {
'default.name': '默认助手',
'default.description': '你可以随时随地和我聊天',
'default.topic.name': '默认话题',
'topics.title': '话题',
'topics.hide_topics': '隐藏话题',
'topics.show_topics': '显示话题',
'topics.auto_rename': 'AI 重命名',
'topics.edit.title': '重命名',
'topics.edit.placeholder': '输入新名称',
'topics.delete.all.title': '删除所有话题',
'topics.delete.all.content': '确定要删除所有话题吗?',
'input.new_chat': ' 新聊天 ',
'input.topics': ' 话题 ',
'input.clear': '清除',
'input.expand': '展开',
'input.collapse': '收起',
'input.clear.title': '清除所有消息?',
'input.clear.content': '确定要清除所有消息吗?',
'input.placeholder': '在这里输入消息...',
'input.send': '发送'
},
apps: {
title: '智能体'
},
provider: {
openai: 'OpenAI',
deepseek: '深度求索',
moonshot: '月之暗面',
silicon: '硅基流动',
openrouter: 'OpenRouter',
yi: '零一万物',
zhipu: '智谱AI',
groq: 'Groq',
ollama: 'Ollama'
},
settings: {
title: '设置',
general: '常规',
provider: '模型提供商',
model: '模型设置',
assistant: '默认助手',
about: '关于'
about: '关于',
'general.title': '常规设置',
'provider.api_key': 'API 密钥',
'provider.check': '检查',
'provider.get_api_key': '点击这里获取密钥',
'provider.api_host': 'API 地址',
'provider.docs_check': '查看',
'provider.docs_more_details': '获取更多详情',
'provider.search_placeholder': '搜索模型 ID 或名称',
'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',
'models.add.model_name': '模型名称',
'models.add.model_name.placeholder': '例如 GPT-3.5',
'models.add.group_name': '分组名称',
'models.add.group_name.tooltip': '例如 ChatGPT',
'models.add.group_name.placeholder': '例如 ChatGPT',
'assistant.title': '默认助手',
'about.description': '一个为创造者而生的 AI 助手'
}
}
}
@ -30,7 +219,7 @@ const resources = {
i18n.use(initReactI18next).init({
resources,
lng: 'en-US',
lng: store.getState().settings.language || 'en-US',
fallbackLng: 'en-US',
interpolation: {
escapeValue: false

View File

@ -9,12 +9,14 @@ import { SystemAssistant } from '@renderer/types'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { colorPrimary } from '@renderer/config/antd'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const assistantGroups = groupBy(SYSTEM_ASSISTANTS, 'group')
const { t } = useTranslation()
const onAddAssistant = (assistant: SystemAssistant) => {
addAssistant({
@ -22,7 +24,7 @@ const AppsPage: FC = () => {
...assistant
})
window.message.success({
content: 'Assistant added successfully',
content: t('message.assistant.added.content'),
key: 'assistant-added',
style: { marginTop: '5vh' }
})
@ -31,7 +33,7 @@ const AppsPage: FC = () => {
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>Assistant Market</NavbarCenter>
<NavbarCenter style={{ borderRight: 'none' }}>{t('apps.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
{Object.keys(assistantGroups).map((group) => (

View File

@ -8,12 +8,14 @@ import { uuid } from '@renderer/utils'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { Tooltip } from 'antd'
import Navigation from './components/Navigation'
import { useTranslation } from 'react-i18next'
const HomePage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const [activeAssistant, setActiveAssistant] = useState(assistants[0])
const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar()
const { defaultAssistant } = useDefaultAssistant()
const { t } = useTranslation()
const onCreateAssistant = () => {
const assistant = { ...defaultAssistant, id: uuid() }
@ -31,7 +33,10 @@ const HomePage: FC = () => {
</NavbarLeft>
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<Tooltip placement="left" title={showRightSidebar ? 'Hide Topics' : 'Show Topics'} arrow>
<Tooltip
placement="left"
title={showRightSidebar ? t('assistant.topics.hide_topics') : t('assistant.topics.show_topics')}
arrow>
<NewButton onClick={setShowRightSidebar}>
<i className={`iconfont ${showRightSidebar ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
</NewButton>

View File

@ -8,6 +8,7 @@ import { droppableReorder, uuid } from '@renderer/utils'
import { Dropdown, MenuProps } from 'antd'
import { last } from 'lodash'
import { FC, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
@ -20,6 +21,8 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
const targetAssistant = useRef<Assistant | null>(null)
const { t } = useTranslation()
const onDelete = (assistant: Assistant) => {
removeAssistant(assistant.id)
setTimeout(() => {
@ -30,7 +33,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
const items: MenuProps['items'] = [
{
label: 'Edit',
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
@ -41,7 +44,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
}
},
{
label: 'Duplicate',
label: t('common.duplicate'),
key: 'duplicate',
icon: <CopyOutlined />,
async onClick() {
@ -52,7 +55,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
},
{ type: 'divider' },
{
label: 'Delete',
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,

View File

@ -1,16 +0,0 @@
import { FC } from 'react'
import styled from 'styled-components'
const ChatSettings: FC = () => {
return (
<Container>
<p>Chat Settings</p>
</Container>
)
}
const Container = styled.div`
width: 300px;
`
export default ChatSettings

View File

@ -3,6 +3,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import { CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
interface CodeBlockProps {
children: string
@ -13,9 +14,11 @@ interface CodeBlockProps {
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
const match = /language-(\w+)/.exec(className || '')
const { t } = useTranslation()
const onCopy = () => {
navigator.clipboard.writeText(children)
window.message.success({ content: 'Copied!', key: 'copy-code' })
window.message.success({ content: t('message.copied'), key: 'copy-code' })
}
return match ? (

View File

@ -20,6 +20,8 @@ import SendMessageSetting from './SendMessageSetting'
import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
import { useAppSelector } from '@renderer/store'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useTranslation } from 'react-i18next'
interface Props {
assistant: Assistant
@ -35,6 +37,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const generating = useAppSelector((state) => state.runtime.generating)
const inputRef = useRef<TextAreaRef>(null)
const { t } = useTranslation()
const sendMessage = () => {
if (generating) {
return
@ -75,11 +79,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
const addNewTopic = useCallback(() => {
const topic: Topic = {
id: uuid(),
name: 'Default Topic',
messages: []
}
const topic: Topic = getDefaultTopic()
addTopic(topic)
setActiveTopic(topic)
}, [addTopic, setActiveTopic])
@ -116,21 +116,21 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<Container id="inputbar" style={{ minHeight: expended ? '35%' : 'var(--input-bar-height)' }}>
<Toolbar>
<ToolbarMenu>
<Tooltip placement="top" title=" New Chat " arrow>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<PlusCircleOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title=" Topics " arrow>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
<ToolbarButton type="text" onClick={setShowRightSidebar}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title=" Clear " arrow>
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
<Popconfirm
icon={false}
title="Clear all messages?"
description="Are you sure to clear all messages?"
title={t('assistant.input.clear.title')}
description={t('assistant.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okText="Clear"
@ -140,7 +140,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title=" Expand " arrow>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
@ -158,12 +158,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message here..."
placeholder={t('assistant.input.placeholder')}
autoFocus
contextMenu="true"
variant="borderless"
styles={{ textarea: { paddingLeft: 0 } }}
allowClear
ref={inputRef}
/>
</Container>

View File

@ -11,6 +11,7 @@ import { getModelLogo } from '@renderer/services/provider'
import Logo from '@renderer/assets/images/logo.png'
import { SyncOutlined } from '@ant-design/icons'
import { firstLetter } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
interface Props {
message: Message
@ -22,21 +23,22 @@ interface Props {
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
const avatar = useAvatar()
const { t } = useTranslation()
const isLastMessage = index === 0
const canRegenerate = isLastMessage && message.role === 'assistant'
const onCopy = () => {
navigator.clipboard.writeText(message.content)
window.message.success({ content: 'Copied!', key: 'copy-message' })
window.message.success({ content: t('message.copied'), key: 'copy-message' })
}
const onDelete = async () => {
const confirmed = await window.modal.confirm({
icon: null,
title: 'Delete Message',
content: 'Are you sure you want to delete this message?',
okText: 'Delete',
title: t('message.message.delete.title'),
content: t('message.message.delete.content'),
okText: t('common.delete'),
okType: 'danger'
})
confirmed && onDeleteMessage?.(message)
@ -80,14 +82,14 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
<EditOutlined onClick={onEdit} />
</Tooltip>
)}
<Tooltip title="Copy" mouseEnterDelay={0.8}>
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<CopyOutlined onClick={onCopy} />
</Tooltip>
<Tooltip title="Delete" mouseEnterDelay={0.8}>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<DeleteOutlined onClick={onDelete} />
</Tooltip>
{canRegenerate && (
<Tooltip title="Regenerate" mouseEnterDelay={0.8}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<SyncOutlined onClick={onRegenerate} />
</Tooltip>
)}

View File

@ -7,10 +7,10 @@ import MessageItem from './Message'
import { reverse } from 'lodash'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant'
import { runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { t } from 'i18next'
interface Props {
assistant: Assistant
@ -47,7 +47,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
)
const autoRenameTopic = useCallback(async () => {
if (topic.name === DEFAULT_TOPIC_NAME && messages.length >= 2) {
if (topic.name === t('assistant.default.topic.name') && messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
summaryText && updateTopic({ ...topic, name: summaryText })
}

View File

@ -5,6 +5,7 @@ import { useProviders } from '@renderer/hooks/useProvider'
import { Assistant } from '@renderer/types'
import { Button, Dropdown, MenuProps } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
@ -14,12 +15,13 @@ interface Props {
const Navigation: FC<Props> = ({ activeAssistant }) => {
const { providers } = useProviders()
const { model, setModel } = useAssistant(activeAssistant.id)
const { t } = useTranslation()
const items: MenuProps['items'] = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
key: p.id,
label: p.name,
label: t(`provider.${p.id}`),
type: 'group',
children: p.models.map((m) => ({
key: m.id,
@ -34,7 +36,7 @@ const Navigation: FC<Props> = ({ activeAssistant }) => {
{activeAssistant?.name}
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
<Button size="small" type="primary" ghost style={{ fontSize: '11px' }}>
{model ? model.name : 'Select Model'}
{model ? model.name : t('button.select_model')}
</Button>
</DropdownMenu>
</NavbarCenter>

View File

@ -2,21 +2,23 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd'
import { FC, PropsWithChildren } from 'react'
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
interface Props extends PropsWithChildren {}
const SendMessageSetting: FC<Props> = ({ children }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation()
const sendSettingItems: MenuProps['items'] = [
{
label: 'Enter Send',
label: `Enter ${t('assistant.input.send')}`,
key: 'Enter',
icon: <EnterOutlined />,
onClick: () => setSendMessageShortcut('Enter')
},
{
label: 'Shift + Enter Send',
label: `Shift+Enter ${t('assistant.input.send')}`,
key: 'Shift+Enter',
icon: <ArrowUpOutlined />,
onClick: () => setSendMessageShortcut('Shift+Enter')

View File

@ -10,6 +10,7 @@ import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/ico
import LocalStorage from '@renderer/services/storage'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
interface Props {
assistant: Assistant
@ -21,10 +22,11 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null)
const { t } = useTranslation()
const topicMenuItems: MenuProps['items'] = [
{
label: 'Auto Rename',
label: t('assistant.topics.auto_rename'),
key: 'auto-rename',
icon: <SignatureOutlined />,
async onClick() {
@ -40,13 +42,13 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
}
},
{
label: 'Rename',
label: t('common.rename'),
key: 'rename',
icon: <EditOutlined />,
async onClick() {
const name = await PromptPopup.show({
title: 'Rename Topic',
message: 'Please enter the new name',
title: t('assistant.topics.edit.title'),
message: t('assistant.topics.edit.placeholder'),
defaultValue: currentTopic.current?.name || ''
})
if (name && currentTopic.current && currentTopic.current?.name !== name) {
@ -59,7 +61,7 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' })
topicMenuItems.push({
label: 'Delete',
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
@ -87,11 +89,13 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
return (
<Container className={showRightSidebar ? '' : 'collapsed'}>
<TopicTitle>
<span>Topics ({assistant.topics.length})</span>
<span>
{t('assistant.topics.title')} ({assistant.topics.length})
</span>
<Popconfirm
icon={false}
title="Delete all topic?"
description="Are you sure to delete all topics?"
title={t('assistant.topics.delete.all.title')}
description={t('assistant.topics.delete.all.content')}
placement="leftBottom"
onConfirm={removeAllTopics}
okText="Delete All"

View File

@ -3,9 +3,11 @@ import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png'
import { runAsyncFunction } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
const AboutSettings: FC = () => {
const [version, setVersion] = useState('')
const { t } = useTranslation()
useEffect(() => {
runAsyncFunction(async () => {
@ -20,7 +22,7 @@ const AboutSettings: FC = () => {
<Title>
Cherry Studio <Version>(v{version})</Version>
</Title>
<Description>A powerful AI assistant for producer.</Description>
<Description>{t('settings.about.description')}</Description>
</Container>
)
}

View File

@ -3,31 +3,34 @@ import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from
import { Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useTranslation } from 'react-i18next'
const AssistantSettings: FC = () => {
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()
const { t } = useTranslation()
return (
<SettingContainer>
<SettingTitle>Default Assistant</SettingTitle>
<SettingTitle>{t('settings.assistant.title')}</SettingTitle>
<SettingDivider />
<SettingSubtitle style={{ marginTop: 0 }}>Name</SettingSubtitle>
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle>
<Input
placeholder="Assistant Name"
placeholder={t('common.assistant') + t('common.name')}
value={defaultAssistant.name}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, name: e.target.value })}
/>
<SettingSubtitle>Description</SettingSubtitle>
<SettingSubtitle>{t('common.description')}</SettingSubtitle>
<TextArea
rows={2}
placeholder="Assistant Description"
placeholder={t('common.assistant') + t('common.description')}
value={defaultAssistant.description}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, description: e.target.value })}
/>
<SettingSubtitle>Prompt</SettingSubtitle>
<SettingSubtitle>{t('common.prompt')}</SettingSubtitle>
<TextArea
rows={4}
placeholder="Assistant Prompt"
placeholder={t('common.assistant') + t('common.prompt')}
value={defaultAssistant.prompt}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
/>

View File

@ -1,25 +1,37 @@
import { FC } from 'react'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
import { Avatar, message, Upload } from 'antd'
import { Avatar, message, Select, Upload } from 'antd'
import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage'
import { compressImage } from '@renderer/utils'
import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store'
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'
const GeneralSettings: FC = () => {
const avatar = useAvatar()
const [messageApi, contextHolder] = message.useMessage()
const { language } = useSettings()
const dispatch = useAppDispatch()
const { t } = useTranslation()
const onSelectLanguage = (value: string) => {
dispatch(setLanguage(value))
i18next.changeLanguage(value)
// window.location.reload()
}
return (
<SettingContainer>
{contextHolder}
<SettingTitle>General Settings</SettingTitle>
<SettingTitle>{t('settings.general.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>Avatar</SettingRowTitle>
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
<Upload
customRequest={() => {}}
accept="image/png, image/jpeg"
@ -42,6 +54,19 @@ const GeneralSettings: FC = () => {
</Upload>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
<Select
defaultValue={language || 'en-US'}
style={{ width: 120 }}
onChange={onSelectLanguage}
options={[
{ value: 'zh-CN', label: '中文' },
{ value: 'en-US', label: 'English' }
]}
/>
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}

View File

@ -5,16 +5,18 @@ import { useProviders } from '@renderer/hooks/useProvider'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { find } from 'lodash'
import { Model } from '@renderer/types'
import { useTranslation } from 'react-i18next'
const ModelSettings: FC = () => {
const { defaultModel, topicNamingModel, setDefaultModel, setTopicNamingModel } = useDefaultModel()
const { providers } = useProviders()
const allModels = providers.map((p) => p.models).flat()
const { t } = useTranslation()
const selectOptions = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
label: p.name,
label: t(`provider.${p.id}`),
title: p.name,
options: p.models.map((m) => ({
label: m.name,
@ -24,7 +26,7 @@ const ModelSettings: FC = () => {
return (
<SettingContainer>
<SettingTitle>Default Assistant Model</SettingTitle>
<SettingTitle>{t('settings.models.default_assistant_model')}</SettingTitle>
<SettingDivider />
<Select
defaultValue={defaultModel.id}
@ -33,7 +35,7 @@ const ModelSettings: FC = () => {
options={selectOptions}
/>
<div style={{ height: 30 }} />
<SettingTitle>Topic Naming Model</SettingTitle>
<SettingTitle>{t('settings.models.topic_naming_model')}</SettingTitle>
<SettingDivider />
<Select
defaultValue={topicNamingModel.id}

View File

@ -7,11 +7,13 @@ import { Avatar, Tag } from 'antd'
import { FC, useState } from 'react'
import styled from 'styled-components'
import ProviderSetting from './components/ProviderSetting'
import { useTranslation } from 'react-i18next'
const ProviderSettings: FC = () => {
const providers = useSystemProviders()
const { updateProviders } = useProviders()
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
const { t } = useTranslation()
const onDragEnd = (result: DropResult) => {
if (result.destination) {
@ -38,7 +40,7 @@ const ProviderSettings: FC = () => {
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
<Avatar src={getProviderLogo(provider.id)} size={22} />
<ProviderItemName>{provider.name}</ProviderItemName>
<ProviderItemName>{t(`provider.${provider.id}`)}</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto' }}>
ON

View File

@ -5,6 +5,7 @@ import { getDefaultGroupName } from '@renderer/utils'
import { Button, Form, FormProps, Input, Modal } from 'antd'
import { find } from 'lodash'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface ShowParams {
title: string
@ -26,6 +27,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm()
const { addModel, models } = useProvider(provider.id)
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
@ -73,12 +75,16 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
colon={false}
style={{ marginTop: 25 }}
onFinish={onFinish}>
<Form.Item label="Provider" name="provider" initialValue={provider.id} rules={[{ required: true }]}>
<Input placeholder="Provider Name" disabled />
<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 label="Model ID" name="id" tooltip="Example: gpt-3.5-turbo" rules={[{ required: true }]}>
<Form.Item
name="id"
label={t('settings.models.add.model_id')}
tooltip={t('settings.models.add.model_id.tooltip')}
rules={[{ required: true }]}>
<Input
placeholder="Required e.g. gpt-3.5-turbo"
placeholder={t('settings.models.add.model_id.placeholder')}
spellCheck={false}
onChange={(e) => {
form.setFieldValue('name', e.target.value.toUpperCase())
@ -86,15 +92,18 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
}}
/>
</Form.Item>
<Form.Item label="Model Name" tooltip="Example: GPT-3.5" name="name">
<Input placeholder="Optional e.g. GPT-4" spellCheck={false} />
<Form.Item name="name" label={t('settings.models.add.model_name')} tooltip="Example: GPT-3.5">
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item label="Group Name" tooltip="Example: ChatGPT" name="group">
<Input placeholder="Optional e.g. OpenAI" spellCheck={false} />
<Form.Item
name="group"
label={t('settings.models.add.group_name')}
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
Add Model
{t('settings.models.add.add_model')}
</Button>
</Form.Item>
</Form>

View File

@ -11,6 +11,7 @@ import { groupBy, isEmpty, uniqBy } from 'lodash'
import { useEffect, useState } from 'react'
import styled from 'styled-components'
import { TopView } from '../../../components/TopView'
import { useTranslation } from 'react-i18next'
interface ShowParams {
provider: Provider
@ -26,6 +27,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
const [listModels, setListModels] = useState<Model[]>([])
const [loading, setLoading] = useState(false)
const [searchText, setSearchText] = useState('')
const { t } = useTranslation()
const systemModels = SYSTEM_MODELS[_provider.id] || []
const allModels = uniqBy([...systemModels, ...listModels, ...models], 'id')
@ -83,7 +85,9 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
const ModalHeader = () => {
return (
<Flex>
<ModelHeaderTitle>{provider.name} Models</ModelHeaderTitle>
<ModelHeaderTitle>
{provider.name} {t('common.models')}
</ModelHeaderTitle>
{loading && <LoadingOutlined size={20} />}
</Flex>
)
@ -103,7 +107,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
header: { padding: 22, paddingBottom: 15 }
}}>
<SearchContainer>
<Search placeholder="Search model id or name" allowClear onSearch={setSearchText} />
<Search placeholder={t('settings.provider.search_placeholder')} allowClear onSearch={setSearchText} />
</SearchContainer>
<ListContainer>
{Object.keys(modelGroups).map((group) => (

View File

@ -11,91 +11,20 @@ import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup'
import Link from 'antd/es/typography/Link'
import { checkApi } from '@renderer/services/api'
import { useTranslation } from 'react-i18next'
import { PROVIDER_CONFIG } from '@renderer/config/provider'
interface Props {
provider: Provider
}
const PROVIDER_CONFIG = {
openai: {
websites: {
official: 'https://openai.com/',
apiKey: 'https://platform.openai.com/api-keys',
docs: 'https://platform.openai.com/docs',
models: 'https://platform.openai.com/docs/models'
}
},
silicon: {
websites: {
official: 'https://www.siliconflow.cn/',
apiKey: 'https://cloud.siliconflow.cn/account/ak',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'
}
},
deepseek: {
websites: {
official: 'https://deepseek.com/',
apiKey: 'https://platform.deepseek.com/api_keys',
docs: 'https://platform.deepseek.com/api-docs/',
models: 'https://platform.deepseek.com/api-docs/'
}
},
yi: {
websites: {
official: 'https://platform.lingyiwanwu.com/',
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
docs: 'https://platform.lingyiwanwu.com/docs',
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
}
},
zhipu: {
websites: {
official: 'https://open.bigmodel.cn/',
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
docs: 'https://open.bigmodel.cn/dev/howuse/introduction',
models: 'https://open.bigmodel.cn/modelcenter/square'
}
},
moonshot: {
websites: {
official: 'https://moonshot.ai/',
apiKey: 'https://platform.moonshot.cn/console/api-keys',
docs: 'https://platform.moonshot.cn/docs/',
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
}
},
openrouter: {
websites: {
official: 'https://openrouter.ai/',
apiKey: 'https://openrouter.ai/settings/keys',
docs: 'https://openrouter.ai/docs/quick-start',
models: 'https://openrouter.ai/docs/models'
}
},
groq: {
websites: {
official: 'https://groq.com/',
apiKey: 'https://console.groq.com/keys',
docs: 'https://console.groq.com/docs/quickstart',
models: 'https://console.groq.com/docs/models'
}
},
ollama: {
websites: {
official: 'https://ollama.com/',
docs: 'https://github.com/ollama/ollama/tree/main/docs',
models: 'https://ollama.com/library'
}
}
}
const ProviderSetting: FC<Props> = ({ provider }) => {
const [apiKey, setApiKey] = useState(provider.apiKey)
const [apiHost, setApiHost] = useState(provider.apiHost)
const [apiValid, setApiValid] = useState(false)
const [apiChecking, setApiChecking] = useState(false)
const { updateProvider, models } = useProvider(provider.id)
const { t } = useTranslation()
const modelGroups = groupBy(models, 'group')
@ -107,7 +36,7 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
const onUpdateApiKey = () => updateProvider({ ...provider, apiKey })
const onUpdateApiHost = () => updateProvider({ ...provider, apiHost })
const onManageModel = () => EditModelsPopup.show({ provider })
const onAddModel = () => AddModelPopup.show({ title: 'Add Model', provider })
const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add_model'), provider })
const onCheckApi = async () => {
setApiChecking(true)
@ -129,7 +58,7 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
<SettingContainer>
<SettingTitle>
<Flex align="center">
<span>{provider.name}</span>
<span>{t(`provider.${provider.id}`)}</span>
{officialWebsite! && (
<Link target="_blank" href={providerConfig.websites.official}>
<ExportOutlined style={{ marginLeft: '8px', color: 'white', fontSize: '12px' }} />
@ -143,11 +72,11 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
/>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
<SettingSubtitle style={{ marginTop: 5 }}>API Key</SettingSubtitle>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%' }}>
<Input
value={apiKey}
placeholder="API Key"
placeholder={t('settings.provider.api_key')}
onChange={(e) => setApiKey(e.target.value)}
onBlur={onUpdateApiKey}
spellCheck={false}
@ -156,27 +85,26 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
/>
{!apiKeyDisabled && (
<Button type={apiValid ? 'primary' : 'default'} ghost={apiValid} onClick={onCheckApi}>
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : 'Check'}
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
</Button>
)}
</Space.Compact>
{apiKeyWebsite && (
<HelpTextRow>
<HelpText>Get API key from: </HelpText>
<HelpLink target="_blank" href={apiKeyWebsite}>
{provider.name}
{t('settings.provider.get_api_key')}
</HelpLink>
</HelpTextRow>
)}
<SettingSubtitle>API Host</SettingSubtitle>
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Input
value={apiHost}
placeholder="API Host"
placeholder={t('settings.provider.api_host')}
disabled={provider.isSystem}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
<SettingSubtitle>Models</SettingSubtitle>
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
{modelGroups[group].map((model) => (
@ -191,23 +119,24 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
))}
{docsWebsite && (
<HelpTextRow>
<HelpText>Check </HelpText>
<HelpText>{t('settings.provider.docs_check')} </HelpText>
<HelpLink target="_blank" href={docsWebsite}>
{provider.name} Docs
{t(`provider.${provider.id}`)}
{t('common.docs')}
</HelpLink>
<HelpText>and</HelpText>
<HelpText>{t('common.and')}</HelpText>
<HelpLink target="_blank" href={modelsWebsite}>
Models
{t('common.models')}
</HelpLink>
<HelpText>for more details</HelpText>
<HelpText>{t('settings.provider.docs_more_details')}</HelpText>
</HelpTextRow>
)}
<Flex gap={10} style={{ marginTop: '10px' }}>
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
Manage
{t('button.manage')}
</Button>
<Button type="default" onClick={onAddModel} icon={<PlusOutlined />}>
Add
{t('button.add')}
</Button>
</Flex>
</SettingContainer>

View File

@ -24,7 +24,7 @@ export const SettingTitle = styled.div`
`
export const SettingSubtitle = styled.div`
font-size: 12px;
font-size: 14px;
color: var(--color-text-2);
margin: 15px 0 10px 0;
user-select: none;

View File

@ -8,6 +8,7 @@ import { takeRight } from 'lodash'
import dayjs from 'dayjs'
import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { t } from 'i18next'
interface FetchChatCompletionParams {
messages: Message[]
@ -121,17 +122,17 @@ export async function checkApi(provider: Provider) {
const style = { marginTop: '3vh' }
if (!provider.apiKey) {
window.message.error({ content: 'Please enter your API key first', key, style })
window.message.error({ content: t('error.enter.api.key'), key, style })
return false
}
if (!provider.apiHost) {
window.message.error({ content: 'Please enter your API host first', key, style })
window.message.error({ content: t('error.enter.api.host'), key, style })
return false
}
if (!model) {
window.message.error({ content: 'Please select a model first', key, style })
window.message.error({ content: t('error.enter.model'), key, style })
return false
}
@ -155,7 +156,7 @@ export async function checkApi(provider: Provider) {
key: 'api-check',
style: { marginTop: '3vh' },
duration: valid ? 2 : 8,
content: valid ? 'API connection successful' : 'API connection failed ' + errorMessage
content: valid ? t('api.connection.successful') : t('api.connection.failed') + ' ' + errorMessage
})
return valid

View File

@ -1,12 +1,13 @@
import { Assistant, Model, Provider, Topic } from '@renderer/types'
import store from '@renderer/store'
import { uuid } from '@renderer/utils'
import i18next from 'i18next'
export function getDefaultAssistant(): Assistant {
return {
id: 'default',
name: 'Default Assistant',
description: "Hello, I'm Default Assistant. You can start chatting with me right away",
name: i18next.t('assistant.default.name'),
description: i18next.t('assistant.default.description'),
prompt: '',
topics: [getDefaultTopic()]
}
@ -15,7 +16,7 @@ export function getDefaultAssistant(): Assistant {
export function getDefaultTopic(): Topic {
return {
id: uuid(),
name: 'Default Topic',
name: i18next.t('assistant.default.topic.name'),
messages: []
}
}

View File

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

View File

@ -102,6 +102,16 @@ const migrate = createMigrate({
]
}
}
},
// @ts-ignore store type is unknown
'7': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
language: navigator.language
}
}
}
})

View File

@ -5,11 +5,13 @@ export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
export interface SettingsState {
showRightSidebar: boolean
sendMessageShortcut: SendMessageShortcut
language: string
}
const initialState: SettingsState = {
showRightSidebar: true,
sendMessageShortcut: 'Enter'
sendMessageShortcut: 'Enter',
language: navigator.language
}
const settingsSlice = createSlice({
@ -21,10 +23,13 @@ const settingsSlice = createSlice({
},
setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => {
state.sendMessageShortcut = action.payload
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload
}
}
})
export const { toggleRightSidebar, setSendMessageShortcut } = settingsSlice.actions
export const { toggleRightSidebar, setSendMessageShortcut, setLanguage } = settingsSlice.actions
export default settingsSlice.reducer