feat: add translation module

This commit is contained in:
kangfenmao 2024-07-28 01:01:15 +08:00
parent 5b123f2c33
commit a267a8d4c3
15 changed files with 532 additions and 16 deletions

View File

@ -9,6 +9,7 @@ import { AntdThemeConfig, getAntdLocale } from './config/antd'
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 TranslatePage from './pages/translate/TranslatePage'
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
@ -21,6 +22,7 @@ function App(): JSX.Element {
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/apps" element={<AppsPage />} /> <Route path="/apps" element={<AppsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/settings/*" element={<SettingsPage />} /> <Route path="/settings/*" element={<SettingsPage />} />
</Routes> </Routes>
</HashRouter> </HashRouter>

View File

@ -0,0 +1,47 @@
@font-face {
font-family: "iconfont"; /* Project id 4563475 */
src: url('iconfont.woff2?t=1722099305424') format('woff2'),
url('iconfont.woff?t=1722099305424') format('woff'),
url('iconfont.ttf?t=1722099305424') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-translate_line:before {
content: "\e7de";
}
.icon-history:before {
content: "\e758";
}
.icon-hidesidebarhoriz:before {
content: "\e8eb";
}
.icon-showsidebarhoriz:before {
content: "\e944";
}
.icon-a-addchat:before {
content: "\e658";
}
.icon-appstore:before {
content: "\e792";
}
.icon-chat:before {
content: "\e615";
}
.icon-setting:before {
content: "\e78e";
}

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,4 @@
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css'; @import '../fonts/icon-fonts/iconfont.css';
@import './markdown.scss'; @import './markdown.scss';
@import './scrollbar.scss'; @import './scrollbar.scss';

View File

@ -4,6 +4,7 @@ import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { TranslationOutlined } from '@ant-design/icons'
const Sidebar: FC = () => { const Sidebar: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
@ -29,6 +30,11 @@ const Sidebar: FC = () => {
<i className="iconfont icon-appstore"></i> <i className="iconfont icon-appstore"></i>
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink to="/translate">
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Menus> </Menus>
</MainMenus> </MainMenus>
<Menus> <Menus>
@ -85,22 +91,28 @@ const Icon = styled.div`
margin-bottom: 5px; margin-bottom: 5px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
-webkit-app-region: none; -webkit-app-region: none;
.iconfont { .iconfont,
.anticon {
color: var(--color-icon); color: var(--color-icon);
font-size: 20px; font-size: 20px;
transition: color 0.2s ease; transition: color 0.2s ease;
text-decoration: none; text-decoration: none;
} }
.anticon {
font-size: 17px;
}
&:hover { &:hover {
background-color: #ffffff30; background-color: #ffffff30;
cursor: pointer; cursor: pointer;
.iconfont { .iconfont,
.anticon {
color: var(--color-icon-white); color: var(--color-icon-white);
} }
} }
&.active { &.active {
background-color: #ffffff20; background-color: #ffffff20;
.iconfont { .iconfont,
.anticon {
color: var(--color-icon-white); color: var(--color-icon-white);
} }
} }

View File

@ -14,7 +14,7 @@ import {
updateTopic, updateTopic,
updateTopics updateTopics
} from '@renderer/store/assistants' } from '@renderer/store/assistants'
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm' import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import localforage from 'localforage' import localforage from 'localforage'
@ -71,13 +71,15 @@ export function useDefaultAssistant() {
} }
export function useDefaultModel() { export function useDefaultModel() {
const { defaultModel, topicNamingModel } = useAppSelector((state) => state.llm) const { defaultModel, topicNamingModel, translateModel } = useAppSelector((state) => state.llm)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return { return {
defaultModel, defaultModel,
topicNamingModel, topicNamingModel,
setDefaultModel: (model: Model) => dispatch(_setDefaultModel({ model })), translateModel,
setTopicNamingModel: (model: Model) => dispatch(_setTopicNamingModel({ model })) setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
setTopicNamingModel: (model: Model) => dispatch(setTopicNamingModel({ model })),
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
} }
} }

View File

@ -102,7 +102,7 @@ const resources = {
title: 'Settings', title: 'Settings',
general: 'General Settings', general: 'General Settings',
provider: 'Model Provider', provider: 'Model Provider',
model: 'Model Settings', model: 'Default Model',
assistant: 'Default Assistant', assistant: 'Default Assistant',
about: 'About & Feedback', about: 'About & Feedback',
'messages.model.title': 'Model Settings', 'messages.model.title': 'Model Settings',
@ -124,6 +124,7 @@ const resources = {
'provider.api.url.reset': 'Reset', 'provider.api.url.reset': 'Reset',
'models.default_assistant_model': 'Default Assistant Model', 'models.default_assistant_model': 'Default Assistant Model',
'models.topic_naming_model': 'Topic Naming Model', 'models.topic_naming_model': 'Topic Naming Model',
'models.translate_model': 'Translate Model',
'models.add.add_model': 'Add Model', 'models.add.add_model': 'Add Model',
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo', 'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
'models.add.model_id': 'Model ID', 'models.add.model_id': 'Model ID',
@ -156,6 +157,27 @@ const resources = {
'about.contact.title': '📧 Contact', 'about.contact.title': '📧 Contact',
'about.contact.button': 'Email', 'about.contact.button': 'Email',
'proxy.title': 'Proxy Address' 'proxy.title': 'Proxy Address'
},
translate: {
title: 'Translation',
'any.language': 'Any language',
'button.translate': 'Translate',
'error.not_configured': 'Translation model is not configured',
'input.placeholder': 'Enter text to translate',
'output.placeholder': 'Translation'
},
languages: {
english: 'English',
chinese: 'Chinese',
'chinese-traditional': 'Traditional Chinese',
japanese: 'Japanese',
korean: 'Korean',
russian: 'Russian',
spanish: 'Spanish',
french: 'French',
italian: 'Italian',
portuguese: 'Portuguese',
arabic: 'Arabic'
} }
} }
}, },
@ -258,7 +280,7 @@ const resources = {
title: '设置', title: '设置',
general: '常规设置', general: '常规设置',
provider: '模型提供商', provider: '模型提供商',
model: '模型设置', model: '默认模型',
assistant: '默认助手', assistant: '默认助手',
about: '关于我们', about: '关于我们',
'messages.model.title': '模型设置', 'messages.model.title': '模型设置',
@ -280,6 +302,7 @@ const resources = {
'provider.api.url.reset': '重置', 'provider.api.url.reset': '重置',
'models.default_assistant_model': '默认助手模型', 'models.default_assistant_model': '默认助手模型',
'models.topic_naming_model': '话题命名模型', 'models.topic_naming_model': '话题命名模型',
'models.translate_model': '翻译模型',
'models.add.add_model': '添加模型', 'models.add.add_model': '添加模型',
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo', 'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
'models.add.model_id': '模型 ID', 'models.add.model_id': '模型 ID',
@ -312,6 +335,27 @@ const resources = {
'about.contact.title': '📧 邮件联系', 'about.contact.title': '📧 邮件联系',
'about.contact.button': '邮件', 'about.contact.button': '邮件',
'proxy.title': '代理地址' 'proxy.title': '代理地址'
},
translate: {
title: '翻译',
'any.language': '任意语言',
'button.translate': '翻译',
'error.not_configured': '翻译模型未配置',
'input.placeholder': '输入文本进行翻译',
'output.placeholder': '翻译'
},
languages: {
english: '英文',
chinese: '简体中文',
'chinese-traditional': '繁体中文',
japanese: '日文',
korean: '韩文',
russian: '俄文',
spanish: '西班牙文',
french: '法文',
italian: '意大利文',
portuguese: '葡萄牙文',
arabic: '阿拉伯文'
} }
} }
} }

View File

@ -6,9 +6,11 @@ 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' import { useTranslation } from 'react-i18next'
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
const ModelSettings: FC = () => { const ModelSettings: FC = () => {
const { defaultModel, topicNamingModel, setDefaultModel, setTopicNamingModel } = useDefaultModel() const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
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 { t } = useTranslation()
@ -24,9 +26,16 @@ const ModelSettings: FC = () => {
})) }))
})) }))
const iconStyle = { fontSize: 16, marginRight: 8 }
return ( return (
<SettingContainer> <SettingContainer>
<SettingTitle>{t('settings.models.default_assistant_model')}</SettingTitle> <SettingTitle>
<div>
<MessageOutlined style={iconStyle} />
{t('settings.models.default_assistant_model')}
</div>
</SettingTitle>
<SettingDivider /> <SettingDivider />
<Select <Select
defaultValue={defaultModel.id} defaultValue={defaultModel.id}
@ -35,7 +44,12 @@ const ModelSettings: FC = () => {
options={selectOptions} options={selectOptions}
/> />
<div style={{ height: 30 }} /> <div style={{ height: 30 }} />
<SettingTitle>{t('settings.models.topic_naming_model')}</SettingTitle> <SettingTitle>
<div>
<EditOutlined style={iconStyle} />
{t('settings.models.topic_naming_model')}
</div>
</SettingTitle>
<SettingDivider /> <SettingDivider />
<Select <Select
defaultValue={topicNamingModel.id} defaultValue={topicNamingModel.id}
@ -43,6 +57,21 @@ const ModelSettings: FC = () => {
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)} onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
options={selectOptions} options={selectOptions}
/> />
<div style={{ height: 30 }} />
<SettingTitle>
<div>
<TranslationOutlined style={iconStyle} />
{t('settings.models.translate_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select
defaultValue={translateModel?.id}
style={{ width: 200 }}
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
options={selectOptions}
placeholder={t('settings.models.empty')}
/>
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -0,0 +1,311 @@
import {
CheckOutlined,
CopyOutlined,
SendOutlined,
SettingOutlined,
SwapOutlined,
WarningOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { fetchTranslate } from '@renderer/services/api'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { Assistant, Message } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Button, Select, Space } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { isEmpty } from 'lodash'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import CodeBlock from '../home/components/CodeBlock'
let _text = ''
let _result = ''
let _targetLanguage = 'english'
const TranslatePage: FC = () => {
const { t } = useTranslation()
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
const [text, setText] = useState(_text)
const [result, setResult] = useState(_result)
const { translateModel } = useDefaultModel()
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
_text = text
_result = result
_targetLanguage = targetLanguage
const languageOptions = [
{
value: 'english',
label: t('languages.english'),
emoji: '🇬🇧'
},
{
value: 'chinese',
label: t('languages.chinese'),
emoji: '🇨🇳'
},
{
value: 'chinese-traditional',
label: t('languages.chinese-traditional'),
emoji: '🇭🇰'
},
{
value: 'japanese',
label: t('languages.japanese'),
emoji: '🇯🇵'
},
{
value: 'korean',
label: t('languages.korean'),
emoji: '🇰🇷'
},
{
value: 'russian',
label: t('languages.russian'),
emoji: '🇷🇺'
},
{
value: 'spanish',
label: t('languages.spanish'),
emoji: '🇪🇸'
},
{
value: 'french',
label: t('languages.french'),
emoji: '🇫🇷'
},
{
value: 'italian',
label: t('languages.italian'),
emoji: '🇮🇹'
},
{
value: 'portuguese',
label: t('languages.portuguese'),
emoji: '🇵🇹'
},
{
value: 'arabic',
label: t('languages.arabic'),
emoji: '🇸🇦'
}
]
const onTranslate = async () => {
if (!text.trim()) {
return
}
if (!translateModel) {
window.message.error({
content: t('translate.error.not_configured'),
key: 'translate-message'
})
return
}
const assistant: Assistant = getDefaultAssistant()
assistant.model = translateModel
assistant.prompt = `Translate from input language to ${targetLanguage}, provide the translation result directly without any explanation, keep original format. If the target language is the same as the source language, do not translate. The text to be translated is as follows:\n\n ${text}`
const message: Message = {
id: uuid(),
role: 'user',
content: text,
assistantId: assistant.id,
topicId: uuid(),
modelId: translateModel.id,
createdAt: new Date().toISOString(),
status: 'sending'
}
setLoading(true)
const translateText = await fetchTranslate({ message, assistant })
setResult(translateText)
setLoading(false)
}
const onCopy = () => {
navigator.clipboard.writeText(result)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
useEffect(() => {
isEmpty(text) && setResult('')
}, [text])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<MenuContainer>
<Select
showSearch
value="any"
style={{ width: 180 }}
optionFilterProp="label"
disabled
options={[{ label: t('translate.any.language'), value: 'any' }]}
/>
<SwapOutlined />
<Select
showSearch
value={targetLanguage}
style={{ width: 180 }}
optionFilterProp="label"
options={languageOptions}
onChange={(value) => setTargetLanguage(value)}
optionRender={(option) => (
<Space>
<span role="img" aria-label={option.data.label}>
{option.data.emoji}
</span>
{option.label}
</Space>
)}
/>
{translateModel && (
<Link to="/settings/model" style={{ color: 'var(--color-text-2)' }}>
<SettingOutlined />
</Link>
)}
{!translateModel && (
<Link to="/settings/model" style={{ marginLeft: -10 }}>
<Button
type="link"
style={{ color: 'var(--color-error)', textDecoration: 'underline' }}
icon={<WarningOutlined />}>
{t('translate.error.not_configured')}
</Button>
</Link>
)}
</MenuContainer>
<TranslateInputWrapper>
<InputContainer>
<Textarea
variant="borderless"
placeholder={t('translate.input.placeholder')}
value={text}
onChange={(e) => setText(e.target.value)}
disabled={loading}
allowClear
/>
<TranslateButton
type="primary"
loading={loading}
onClick={onTranslate}
disabled={!text.trim()}
icon={<SendOutlined />}>
{t('translate.button.translate')}
</TranslateButton>
</InputContainer>
<OutputContainer>
<OutputText>
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
{result || t('translate.output.placeholder')}
</Markdown>
</OutputText>
<CopyButton
onClick={onCopy}
disabled={!result}
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyOutlined />}
/>
</OutputContainer>
</TranslateInputWrapper>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 20px;
`
const MenuContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 15px;
gap: 20px;
`
const TranslateInputWrapper = styled.div`
display: flex;
flex-direction: row;
min-height: 350px;
gap: 20px;
`
const InputContainer = styled.div`
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
border: 1px solid var(--color-border);
border-radius: 10px;
`
const Textarea = styled(TextArea)`
display: flex;
flex: 1;
padding: 20px;
font-size: 16px;
overflow: auto;
.ant-input {
resize: none;
padding: 15px 20px;
}
`
const OutputContainer = styled.div`
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 10px;
background-color: var(--color-background-soft);
border-radius: 10px;
`
const OutputText = styled.div`
padding: 5px 10px;
max-height: calc(100vh - var(--navbar-height) - 120px);
overflow: auto;
`
const TranslateButton = styled(Button)`
position: absolute;
right: 15px;
bottom: 15px;
z-index: 10;
`
const CopyButton = styled(Button)`
position: absolute;
right: 15px;
bottom: 15px;
`
export default TranslatePage

View File

@ -73,6 +73,30 @@ export default class ProviderSDK {
} }
} }
public async translate(message: Message, assistant: Assistant) {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const messages = [{ role: 'system', content: assistant.prompt }, message]
if (this.isAnthropic) {
const response = await this.anthropicSdk.messages.create({
model: model.id,
messages: messages as MessageParam[],
max_tokens: 4096,
temperature: assistant?.settings?.temperature,
stream: false
})
return response.content[0].type === 'text' ? response.content[0].text : ''
} else {
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false
})
return response.choices[0].message?.content || ''
}
}
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> { public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
const model = getTopNamingModel() || assistant.model || getDefaultModel() const model = getTopNamingModel() || assistant.model || getDefaultModel()

View File

@ -4,9 +4,16 @@ import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Provider, Topic } from '@renderer/types' import { Assistant, Message, Provider, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getAssistantProvider, getDefaultModel, getProviderByModel, getTopNamingModel } from './assistant' import {
getAssistantProvider,
getDefaultModel,
getProviderByModel,
getTopNamingModel,
getTranslateModel
} from './assistant'
import { EVENT_NAMES, EventEmitter } from './event' import { EVENT_NAMES, EventEmitter } from './event'
import ProviderSDK from './ProviderSDK' import ProviderSDK from './ProviderSDK'
import { isEmpty } from 'lodash'
export async function fetchChatCompletion({ export async function fetchChatCompletion({
messages, messages,
@ -63,11 +70,33 @@ export async function fetchChatCompletion({
return message return message
} }
export async function fetchTranslate({ message, assistant }: { message: Message; assistant: Assistant }) {
const model = getTranslateModel()
if (!model) {
return ''
}
const provider = getProviderByModel(model)
if (!hasApiKey(provider)) {
return ''
}
const providerSdk = new ProviderSDK(provider)
try {
return await providerSdk.translate(message, assistant)
} catch (error: any) {
return ''
}
}
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) { export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
const model = getTopNamingModel() || assistant.model || getDefaultModel() const model = getTopNamingModel() || assistant.model || getDefaultModel()
const provider = getProviderByModel(model) const provider = getProviderByModel(model)
if (provider.id !== 'ollama' && !provider.apiKey) { if (!hasApiKey(provider)) {
return null return null
} }
@ -114,6 +143,12 @@ export async function checkApi(provider: Provider) {
return valid return valid
} }
function hasApiKey(provider: Provider) {
if (!provider) return false
if (provider.id === 'ollama') return true
return !isEmpty(provider.apiKey)
}
export async function fetchModels(provider: Provider) { export async function fetchModels(provider: Provider) {
const providerSdk = new ProviderSDK(provider) const providerSdk = new ProviderSDK(provider)

View File

@ -32,6 +32,10 @@ export function getTopNamingModel() {
return store.getState().llm.topicNamingModel return store.getState().llm.topicNamingModel
} }
export function getTranslateModel() {
return store.getState().llm.translateModel
}
export function getAssistantProvider(assistant: Assistant) { export function getAssistantProvider(assistant: Assistant) {
const providers = store.getState().llm.providers const providers = store.getState().llm.providers
const provider = providers.find((p) => p.id === assistant.model?.provider) const provider = providers.find((p) => p.id === assistant.model?.provider)

View File

@ -7,11 +7,13 @@ export interface LlmState {
providers: Provider[] providers: Provider[]
defaultModel: Model defaultModel: Model
topicNamingModel: Model topicNamingModel: Model
translateModel: Model
} }
const initialState: LlmState = { const initialState: LlmState = {
defaultModel: SYSTEM_MODELS.openai[0], defaultModel: SYSTEM_MODELS.openai[0],
topicNamingModel: SYSTEM_MODELS.openai[0], topicNamingModel: SYSTEM_MODELS.openai[0],
translateModel: SYSTEM_MODELS.openai[0],
providers: [ providers: [
{ {
id: 'openai', id: 'openai',
@ -174,6 +176,9 @@ const settingsSlice = createSlice({
}, },
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => { setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
state.topicNamingModel = action.payload.model state.topicNamingModel = action.payload.model
},
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
state.translateModel = action.payload.model
} }
} }
}) })
@ -186,7 +191,8 @@ export const {
addModel, addModel,
removeModel, removeModel,
setDefaultModel, setDefaultModel,
setTopicNamingModel setTopicNamingModel,
setTranslateModel
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer