feat: add translation module
This commit is contained in:
parent
5b123f2c33
commit
a267a8d4c3
@ -9,6 +9,7 @@ import { AntdThemeConfig, getAntdLocale } from './config/antd'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
@ -21,6 +22,7 @@ function App(): JSX.Element {
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
||||
47
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal file
47
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal 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";
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
Binary file not shown.
@ -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 './scrollbar.scss';
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import styled from 'styled-components'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { TranslationOutlined } from '@ant-design/icons'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
@ -29,6 +30,11 @@ const Sidebar: FC = () => {
|
||||
<i className="iconfont icon-appstore"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/translate">
|
||||
<Icon className={isRoute('/translate')}>
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus>
|
||||
@ -85,22 +91,28 @@ const Icon = styled.div`
|
||||
margin-bottom: 5px;
|
||||
transition: background-color 0.2s ease;
|
||||
-webkit-app-region: none;
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 20px;
|
||||
transition: color 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 17px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #ffffff30;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: #ffffff20;
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
updateTopic,
|
||||
updateTopics
|
||||
} 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 localforage from 'localforage'
|
||||
|
||||
@ -71,13 +71,15 @@ export function useDefaultAssistant() {
|
||||
}
|
||||
|
||||
export function useDefaultModel() {
|
||||
const { defaultModel, topicNamingModel } = useAppSelector((state) => state.llm)
|
||||
const { defaultModel, topicNamingModel, translateModel } = useAppSelector((state) => state.llm)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
defaultModel,
|
||||
topicNamingModel,
|
||||
setDefaultModel: (model: Model) => dispatch(_setDefaultModel({ model })),
|
||||
setTopicNamingModel: (model: Model) => dispatch(_setTopicNamingModel({ model }))
|
||||
translateModel,
|
||||
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
|
||||
setTopicNamingModel: (model: Model) => dispatch(setTopicNamingModel({ model })),
|
||||
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ const resources = {
|
||||
title: 'Settings',
|
||||
general: 'General Settings',
|
||||
provider: 'Model Provider',
|
||||
model: 'Model Settings',
|
||||
model: 'Default Model',
|
||||
assistant: 'Default Assistant',
|
||||
about: 'About & Feedback',
|
||||
'messages.model.title': 'Model Settings',
|
||||
@ -124,6 +124,7 @@ const resources = {
|
||||
'provider.api.url.reset': 'Reset',
|
||||
'models.default_assistant_model': 'Default Assistant Model',
|
||||
'models.topic_naming_model': 'Topic Naming Model',
|
||||
'models.translate_model': 'Translate Model',
|
||||
'models.add.add_model': 'Add Model',
|
||||
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
|
||||
'models.add.model_id': 'Model ID',
|
||||
@ -156,6 +157,27 @@ const resources = {
|
||||
'about.contact.title': '📧 Contact',
|
||||
'about.contact.button': 'Email',
|
||||
'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: '设置',
|
||||
general: '常规设置',
|
||||
provider: '模型提供商',
|
||||
model: '模型设置',
|
||||
model: '默认模型',
|
||||
assistant: '默认助手',
|
||||
about: '关于我们',
|
||||
'messages.model.title': '模型设置',
|
||||
@ -280,6 +302,7 @@ const resources = {
|
||||
'provider.api.url.reset': '重置',
|
||||
'models.default_assistant_model': '默认助手模型',
|
||||
'models.topic_naming_model': '话题命名模型',
|
||||
'models.translate_model': '翻译模型',
|
||||
'models.add.add_model': '添加模型',
|
||||
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
|
||||
'models.add.model_id': '模型 ID',
|
||||
@ -312,6 +335,27 @@ const resources = {
|
||||
'about.contact.title': '📧 邮件联系',
|
||||
'about.contact.button': '邮件',
|
||||
'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: '阿拉伯文'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,11 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { find } from 'lodash'
|
||||
import { Model } from '@renderer/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
|
||||
const ModelSettings: FC = () => {
|
||||
const { defaultModel, topicNamingModel, setDefaultModel, setTopicNamingModel } = useDefaultModel()
|
||||
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
|
||||
useDefaultModel()
|
||||
const { providers } = useProviders()
|
||||
const allModels = providers.map((p) => p.models).flat()
|
||||
const { t } = useTranslation()
|
||||
@ -24,9 +26,16 @@ const ModelSettings: FC = () => {
|
||||
}))
|
||||
}))
|
||||
|
||||
const iconStyle = { fontSize: 16, marginRight: 8 }
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.models.default_assistant_model')}</SettingTitle>
|
||||
<SettingTitle>
|
||||
<div>
|
||||
<MessageOutlined style={iconStyle} />
|
||||
{t('settings.models.default_assistant_model')}
|
||||
</div>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={defaultModel.id}
|
||||
@ -35,7 +44,12 @@ const ModelSettings: FC = () => {
|
||||
options={selectOptions}
|
||||
/>
|
||||
<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 />
|
||||
<Select
|
||||
defaultValue={topicNamingModel.id}
|
||||
@ -43,6 +57,21 @@ const ModelSettings: FC = () => {
|
||||
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
311
src/renderer/src/pages/translate/TranslatePage.tsx
Normal file
311
src/renderer/src/pages/translate/TranslatePage.tsx
Normal 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
|
||||
@ -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> {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
|
||||
|
||||
@ -4,9 +4,16 @@ import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Message, Provider, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
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 ProviderSDK from './ProviderSDK'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
export async function fetchChatCompletion({
|
||||
messages,
|
||||
@ -63,11 +70,33 @@ export async function fetchChatCompletion({
|
||||
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 }) {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (provider.id !== 'ollama' && !provider.apiKey) {
|
||||
if (!hasApiKey(provider)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -114,6 +143,12 @@ export async function checkApi(provider: Provider) {
|
||||
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) {
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
|
||||
|
||||
@ -32,6 +32,10 @@ export function getTopNamingModel() {
|
||||
return store.getState().llm.topicNamingModel
|
||||
}
|
||||
|
||||
export function getTranslateModel() {
|
||||
return store.getState().llm.translateModel
|
||||
}
|
||||
|
||||
export function getAssistantProvider(assistant: Assistant) {
|
||||
const providers = store.getState().llm.providers
|
||||
const provider = providers.find((p) => p.id === assistant.model?.provider)
|
||||
|
||||
@ -7,11 +7,13 @@ export interface LlmState {
|
||||
providers: Provider[]
|
||||
defaultModel: Model
|
||||
topicNamingModel: Model
|
||||
translateModel: Model
|
||||
}
|
||||
|
||||
const initialState: LlmState = {
|
||||
defaultModel: SYSTEM_MODELS.openai[0],
|
||||
topicNamingModel: SYSTEM_MODELS.openai[0],
|
||||
translateModel: SYSTEM_MODELS.openai[0],
|
||||
providers: [
|
||||
{
|
||||
id: 'openai',
|
||||
@ -174,6 +176,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||
state.topicNamingModel = action.payload.model
|
||||
},
|
||||
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||
state.translateModel = action.payload.model
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -186,7 +191,8 @@ export const {
|
||||
addModel,
|
||||
removeModel,
|
||||
setDefaultModel,
|
||||
setTopicNamingModel
|
||||
setTopicNamingModel,
|
||||
setTranslateModel
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user