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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,217 @@
import store from '@renderer/store'
import i18n from 'i18next' import i18n from 'i18next'
import { initReactI18next } from 'react-i18next' import { initReactI18next } from 'react-i18next'
const resources = { const resources = {
'en-US': { 'en-US': {
translation: { 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: { settings: {
title: 'Settings', title: 'Settings',
general: 'General', general: 'General',
provider: 'Model Provider', provider: 'Model Provider',
model: 'Model Settings', model: 'Model Settings',
assistant: 'Default Assistant', 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': { 'zh-CN': {
translation: { 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: { settings: {
title: '设置', title: '设置',
general: '常规', general: '常规',
provider: '模型提供商', provider: '模型提供商',
model: '模型设置', model: '模型设置',
assistant: '默认助手', 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({ i18n.use(initReactI18next).init({
resources, resources,
lng: 'en-US', lng: store.getState().settings.language || 'en-US',
fallbackLng: 'en-US', fallbackLng: 'en-US',
interpolation: { interpolation: {
escapeValue: false escapeValue: false

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import { droppableReorder, uuid } from '@renderer/utils'
import { Dropdown, MenuProps } from 'antd' import { Dropdown, MenuProps } from 'antd'
import { last } from 'lodash' import { last } from 'lodash'
import { FC, useRef } from 'react' import { FC, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
@ -20,6 +21,8 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants() const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
const targetAssistant = useRef<Assistant | null>(null) const targetAssistant = useRef<Assistant | null>(null)
const { t } = useTranslation()
const onDelete = (assistant: Assistant) => { const onDelete = (assistant: Assistant) => {
removeAssistant(assistant.id) removeAssistant(assistant.id)
setTimeout(() => { setTimeout(() => {
@ -30,7 +33,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
const items: MenuProps['items'] = [ const items: MenuProps['items'] = [
{ {
label: 'Edit', label: t('common.edit'),
key: 'edit', key: 'edit',
icon: <EditOutlined />, icon: <EditOutlined />,
async onClick() { async onClick() {
@ -41,7 +44,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
} }
}, },
{ {
label: 'Duplicate', label: t('common.duplicate'),
key: 'duplicate', key: 'duplicate',
icon: <CopyOutlined />, icon: <CopyOutlined />,
async onClick() { async onClick() {
@ -52,7 +55,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
}, },
{ type: 'divider' }, { type: 'divider' },
{ {
label: 'Delete', label: t('common.delete'),
key: 'delete', key: 'delete',
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
danger: true, 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 { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components' import styled from 'styled-components'
import { CopyOutlined } from '@ant-design/icons' import { CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
interface CodeBlockProps { interface CodeBlockProps {
children: string children: string
@ -13,9 +14,11 @@ interface CodeBlockProps {
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => { const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
const { t } = useTranslation()
const onCopy = () => { const onCopy = () => {
navigator.clipboard.writeText(children) navigator.clipboard.writeText(children)
window.message.success({ content: 'Copied!', key: 'copy-code' }) window.message.success({ content: t('message.copied'), key: 'copy-code' })
} }
return match ? ( return match ? (

View File

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

View File

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

View File

@ -7,10 +7,10 @@ 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 { DEFAULT_TOPIC_NAME } from '@renderer/config/constant'
import { runAsyncFunction } from '@renderer/utils' import { 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'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@ -47,7 +47,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
) )
const autoRenameTopic = useCallback(async () => { 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 }) const summaryText = await fetchMessagesSummary({ messages, assistant })
summaryText && updateTopic({ ...topic, name: summaryText }) summaryText && updateTopic({ ...topic, name: summaryText })
} }

View File

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

View File

@ -2,21 +2,23 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd' import { Dropdown, MenuProps } from 'antd'
import { FC, PropsWithChildren } from 'react' import { FC, PropsWithChildren } from 'react'
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons' import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
interface Props extends PropsWithChildren {} interface Props extends PropsWithChildren {}
const SendMessageSetting: FC<Props> = ({ children }) => { const SendMessageSetting: FC<Props> = ({ children }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings() const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation()
const sendSettingItems: MenuProps['items'] = [ const sendSettingItems: MenuProps['items'] = [
{ {
label: 'Enter Send', label: `Enter ${t('assistant.input.send')}`,
key: 'Enter', key: 'Enter',
icon: <EnterOutlined />, icon: <EnterOutlined />,
onClick: () => setSendMessageShortcut('Enter') onClick: () => setSendMessageShortcut('Enter')
}, },
{ {
label: 'Shift + Enter Send', label: `Shift+Enter ${t('assistant.input.send')}`,
key: 'Shift+Enter', key: 'Shift+Enter',
icon: <ArrowUpOutlined />, icon: <ArrowUpOutlined />,
onClick: () => setSendMessageShortcut('Shift+Enter') 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 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'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@ -21,10 +22,11 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar() const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id) const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null) const currentTopic = useRef<Topic | null>(null)
const { t } = useTranslation()
const topicMenuItems: MenuProps['items'] = [ const topicMenuItems: MenuProps['items'] = [
{ {
label: 'Auto Rename', label: t('assistant.topics.auto_rename'),
key: 'auto-rename', key: 'auto-rename',
icon: <SignatureOutlined />, icon: <SignatureOutlined />,
async onClick() { async onClick() {
@ -40,13 +42,13 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
} }
}, },
{ {
label: '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: 'Rename Topic', title: t('assistant.topics.edit.title'),
message: 'Please enter the new name', message: t('assistant.topics.edit.placeholder'),
defaultValue: currentTopic.current?.name || '' defaultValue: currentTopic.current?.name || ''
}) })
if (name && currentTopic.current && currentTopic.current?.name !== 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) { if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' }) topicMenuItems.push({ type: 'divider' })
topicMenuItems.push({ topicMenuItems.push({
label: 'Delete', label: t('common.delete'),
danger: true, danger: true,
key: 'delete', key: 'delete',
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
@ -87,11 +89,13 @@ const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
return ( return (
<Container className={showRightSidebar ? '' : 'collapsed'}> <Container className={showRightSidebar ? '' : 'collapsed'}>
<TopicTitle> <TopicTitle>
<span>Topics ({assistant.topics.length})</span> <span>
{t('assistant.topics.title')} ({assistant.topics.length})
</span>
<Popconfirm <Popconfirm
icon={false} icon={false}
title="Delete all topic?" title={t('assistant.topics.delete.all.title')}
description="Are you sure to delete all topics?" description={t('assistant.topics.delete.all.content')}
placement="leftBottom" placement="leftBottom"
onConfirm={removeAllTopics} onConfirm={removeAllTopics}
okText="Delete All" okText="Delete All"

View File

@ -3,9 +3,11 @@ 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'
const AboutSettings: FC = () => { const AboutSettings: FC = () => {
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
@ -20,7 +22,7 @@ const AboutSettings: FC = () => {
<Title> <Title>
Cherry Studio <Version>(v{version})</Version> Cherry Studio <Version>(v{version})</Version>
</Title> </Title>
<Description>A powerful AI assistant for producer.</Description> <Description>{t('settings.about.description')}</Description>
</Container> </Container>
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,91 +11,20 @@ import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup' import EditModelsPopup from './EditModelsPopup'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import { checkApi } from '@renderer/services/api' import { checkApi } from '@renderer/services/api'
import { useTranslation } from 'react-i18next'
import { PROVIDER_CONFIG } from '@renderer/config/provider'
interface Props { interface Props {
provider: Provider 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 ProviderSetting: FC<Props> = ({ provider }) => {
const [apiKey, setApiKey] = useState(provider.apiKey) const [apiKey, setApiKey] = useState(provider.apiKey)
const [apiHost, setApiHost] = useState(provider.apiHost) const [apiHost, setApiHost] = useState(provider.apiHost)
const [apiValid, setApiValid] = useState(false) const [apiValid, setApiValid] = useState(false)
const [apiChecking, setApiChecking] = useState(false) const [apiChecking, setApiChecking] = useState(false)
const { updateProvider, models } = useProvider(provider.id) const { updateProvider, models } = useProvider(provider.id)
const { t } = useTranslation()
const modelGroups = groupBy(models, 'group') const modelGroups = groupBy(models, 'group')
@ -107,7 +36,7 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
const onUpdateApiKey = () => updateProvider({ ...provider, apiKey }) const onUpdateApiKey = () => updateProvider({ ...provider, apiKey })
const onUpdateApiHost = () => updateProvider({ ...provider, apiHost }) const onUpdateApiHost = () => updateProvider({ ...provider, apiHost })
const onManageModel = () => EditModelsPopup.show({ provider }) 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 () => { const onCheckApi = async () => {
setApiChecking(true) setApiChecking(true)
@ -129,7 +58,7 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
<SettingContainer> <SettingContainer>
<SettingTitle> <SettingTitle>
<Flex align="center"> <Flex align="center">
<span>{provider.name}</span> <span>{t(`provider.${provider.id}`)}</span>
{officialWebsite! && ( {officialWebsite! && (
<Link target="_blank" href={providerConfig.websites.official}> <Link target="_blank" href={providerConfig.websites.official}>
<ExportOutlined style={{ marginLeft: '8px', color: 'white', fontSize: '12px' }} /> <ExportOutlined style={{ marginLeft: '8px', color: 'white', fontSize: '12px' }} />
@ -143,11 +72,11 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
/> />
</SettingTitle> </SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} /> <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%' }}> <Space.Compact style={{ width: '100%' }}>
<Input <Input
value={apiKey} value={apiKey}
placeholder="API Key" placeholder={t('settings.provider.api_key')}
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
onBlur={onUpdateApiKey} onBlur={onUpdateApiKey}
spellCheck={false} spellCheck={false}
@ -156,27 +85,26 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
/> />
{!apiKeyDisabled && ( {!apiKeyDisabled && (
<Button type={apiValid ? 'primary' : 'default'} ghost={apiValid} onClick={onCheckApi}> <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> </Button>
)} )}
</Space.Compact> </Space.Compact>
{apiKeyWebsite && ( {apiKeyWebsite && (
<HelpTextRow> <HelpTextRow>
<HelpText>Get API key from: </HelpText>
<HelpLink target="_blank" href={apiKeyWebsite}> <HelpLink target="_blank" href={apiKeyWebsite}>
{provider.name} {t('settings.provider.get_api_key')}
</HelpLink> </HelpLink>
</HelpTextRow> </HelpTextRow>
)} )}
<SettingSubtitle>API Host</SettingSubtitle> <SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Input <Input
value={apiHost} value={apiHost}
placeholder="API Host" placeholder={t('settings.provider.api_host')}
disabled={provider.isSystem} disabled={provider.isSystem}
onChange={(e) => setApiHost(e.target.value)} onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost} onBlur={onUpdateApiHost}
/> />
<SettingSubtitle>Models</SettingSubtitle> <SettingSubtitle>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => ( {Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small"> <Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
{modelGroups[group].map((model) => ( {modelGroups[group].map((model) => (
@ -191,23 +119,24 @@ const ProviderSetting: FC<Props> = ({ provider }) => {
))} ))}
{docsWebsite && ( {docsWebsite && (
<HelpTextRow> <HelpTextRow>
<HelpText>Check </HelpText> <HelpText>{t('settings.provider.docs_check')} </HelpText>
<HelpLink target="_blank" href={docsWebsite}> <HelpLink target="_blank" href={docsWebsite}>
{provider.name} Docs {t(`provider.${provider.id}`)}
{t('common.docs')}
</HelpLink> </HelpLink>
<HelpText>and</HelpText> <HelpText>{t('common.and')}</HelpText>
<HelpLink target="_blank" href={modelsWebsite}> <HelpLink target="_blank" href={modelsWebsite}>
Models {t('common.models')}
</HelpLink> </HelpLink>
<HelpText>for more details</HelpText> <HelpText>{t('settings.provider.docs_more_details')}</HelpText>
</HelpTextRow> </HelpTextRow>
)} )}
<Flex gap={10} style={{ marginTop: '10px' }}> <Flex gap={10} style={{ marginTop: '10px' }}>
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}> <Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
Manage {t('button.manage')}
</Button> </Button>
<Button type="default" onClick={onAddModel} icon={<PlusOutlined />}> <Button type="default" onClick={onAddModel} icon={<PlusOutlined />}>
Add {t('button.add')}
</Button> </Button>
</Flex> </Flex>
</SettingContainer> </SettingContainer>

View File

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

View File

@ -8,6 +8,7 @@ import { takeRight } from 'lodash'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import store from '@renderer/store' import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { t } from 'i18next'
interface FetchChatCompletionParams { interface FetchChatCompletionParams {
messages: Message[] messages: Message[]
@ -121,17 +122,17 @@ export async function checkApi(provider: Provider) {
const style = { marginTop: '3vh' } const style = { marginTop: '3vh' }
if (!provider.apiKey) { 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 return false
} }
if (!provider.apiHost) { 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 return false
} }
if (!model) { 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 return false
} }
@ -155,7 +156,7 @@ export async function checkApi(provider: Provider) {
key: 'api-check', key: 'api-check',
style: { marginTop: '3vh' }, style: { marginTop: '3vh' },
duration: valid ? 2 : 8, 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 return valid

View File

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

View File

@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 6, version: 7,
blacklist: ['runtime'], blacklist: ['runtime'],
migrate 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 { export interface SettingsState {
showRightSidebar: boolean showRightSidebar: boolean
sendMessageShortcut: SendMessageShortcut sendMessageShortcut: SendMessageShortcut
language: string
} }
const initialState: SettingsState = { const initialState: SettingsState = {
showRightSidebar: true, showRightSidebar: true,
sendMessageShortcut: 'Enter' sendMessageShortcut: 'Enter',
language: navigator.language
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -21,10 +23,13 @@ const settingsSlice = createSlice({
}, },
setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => { setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => {
state.sendMessageShortcut = action.payload 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 export default settingsSlice.reducer