feat: translate support stream output
This commit is contained in:
parent
afc2e2f595
commit
aecc5fefcf
59
src/renderer/src/config/translate.ts
Normal file
59
src/renderer/src/config/translate.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
export const TranslateLanguageOptions = [
|
||||
{
|
||||
value: 'english',
|
||||
label: i18n.t('languages.english'),
|
||||
emoji: '🇬🇧'
|
||||
},
|
||||
{
|
||||
value: 'chinese',
|
||||
label: i18n.t('languages.chinese'),
|
||||
emoji: '🇨🇳'
|
||||
},
|
||||
{
|
||||
value: 'chinese-traditional',
|
||||
label: i18n.t('languages.chinese-traditional'),
|
||||
emoji: '🇭🇰'
|
||||
},
|
||||
{
|
||||
value: 'japanese',
|
||||
label: i18n.t('languages.japanese'),
|
||||
emoji: '🇯🇵'
|
||||
},
|
||||
{
|
||||
value: 'korean',
|
||||
label: i18n.t('languages.korean'),
|
||||
emoji: '🇰🇷'
|
||||
},
|
||||
{
|
||||
value: 'russian',
|
||||
label: i18n.t('languages.russian'),
|
||||
emoji: '🇷🇺'
|
||||
},
|
||||
{
|
||||
value: 'spanish',
|
||||
label: i18n.t('languages.spanish'),
|
||||
emoji: '🇪🇸'
|
||||
},
|
||||
{
|
||||
value: 'french',
|
||||
label: i18n.t('languages.french'),
|
||||
emoji: '🇫🇷'
|
||||
},
|
||||
{
|
||||
value: 'italian',
|
||||
label: i18n.t('languages.italian'),
|
||||
emoji: '🇮🇹'
|
||||
},
|
||||
{
|
||||
value: 'portuguese',
|
||||
label: i18n.t('languages.portuguese'),
|
||||
emoji: '🇵🇹'
|
||||
},
|
||||
{
|
||||
value: 'arabic',
|
||||
label: i18n.t('languages.arabic'),
|
||||
emoji: '🇸🇦'
|
||||
}
|
||||
]
|
||||
@ -2,6 +2,7 @@ import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutl
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import db from '@renderer/databases'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
@ -33,64 +34,6 @@ const TranslatePage: FC = () => {
|
||||
_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
|
||||
@ -119,8 +62,7 @@ const TranslatePage: FC = () => {
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const translateText = await fetchTranslate({ message, assistant })
|
||||
setResult(translateText)
|
||||
await fetchTranslate({ message, assistant, onResponse: (text) => setResult(text) })
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@ -187,7 +129,7 @@ const TranslatePage: FC = () => {
|
||||
value={targetLanguage}
|
||||
style={{ width: 180 }}
|
||||
optionFilterProp="label"
|
||||
options={languageOptions}
|
||||
options={TranslateLanguageOptions}
|
||||
onChange={(value) => {
|
||||
setTargetLanguage(value)
|
||||
db.settings.put({ id: 'translate:target:language', value })
|
||||
|
||||
@ -20,8 +20,8 @@ export default class AiProvider {
|
||||
return this.sdk.completions({ messages, assistant, onChunk, onFilterMessages })
|
||||
}
|
||||
|
||||
public async translate(message: Message, assistant: Assistant): Promise<string> {
|
||||
return this.sdk.translate(message, assistant)
|
||||
public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void): Promise<string> {
|
||||
return this.sdk.translate(message, assistant, onResponse)
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
|
||||
|
||||
@ -149,7 +149,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
})
|
||||
}
|
||||
|
||||
public async translate(message: Message, assistant: Assistant) {
|
||||
public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const messages = [
|
||||
@ -157,16 +157,33 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
{ role: 'user', content: message.content }
|
||||
]
|
||||
|
||||
const response = await this.sdk.messages.create({
|
||||
const stream = onResponse ? true : false
|
||||
|
||||
const body: MessageCreateParamsNonStreaming = {
|
||||
model: model.id,
|
||||
messages: messages.filter((m) => m.role === 'user') as MessageParam[],
|
||||
max_tokens: 4096,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
system: assistant.prompt,
|
||||
stream: false
|
||||
})
|
||||
system: assistant.prompt
|
||||
}
|
||||
|
||||
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||
if (!stream) {
|
||||
const response = await this.sdk.messages.create({ ...body, stream: false })
|
||||
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||
}
|
||||
|
||||
let text = ''
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.sdk.messages
|
||||
.stream({ ...body, stream: true })
|
||||
.on('text', (_text) => {
|
||||
text += _text
|
||||
onResponse?.(text)
|
||||
})
|
||||
.on('finalMessage', () => resolve(text))
|
||||
.on('error', (error) => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
|
||||
|
||||
@ -20,7 +20,7 @@ export default abstract class BaseProvider {
|
||||
}
|
||||
|
||||
abstract completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
|
||||
abstract translate(message: Message, assistant: Assistant): Promise<string>
|
||||
abstract translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void): Promise<string>
|
||||
abstract summaries(messages: Message[], assistant: Assistant): Promise<string>
|
||||
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
|
||||
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
|
||||
|
||||
@ -230,7 +230,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async translate(message: Message, assistant: Assistant) {
|
||||
async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const { maxTokens } = getAssistantSettings(assistant)
|
||||
const model = assistant.model || defaultModel
|
||||
@ -247,9 +247,21 @@ export default class GeminiProvider extends BaseProvider {
|
||||
this.requestOptions
|
||||
)
|
||||
|
||||
const { response } = await geminiModel.generateContent(message.content)
|
||||
if (!onResponse) {
|
||||
const { response } = await geminiModel.generateContent(message.content)
|
||||
return response.text()
|
||||
}
|
||||
|
||||
return response.text()
|
||||
const response = await geminiModel.generateContentStream(message.content)
|
||||
|
||||
let text = ''
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
text += chunk.text()
|
||||
onResponse(text)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
|
||||
|
||||
@ -192,7 +192,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async translate(message: Message, assistant: Assistant) {
|
||||
async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const messages = [
|
||||
@ -200,16 +200,41 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
{ role: 'user', content: message.content }
|
||||
]
|
||||
|
||||
const isOpenAIo1 = model.id.startsWith('o1')
|
||||
|
||||
const isSupportedStreamOutput = () => {
|
||||
if (!onResponse) {
|
||||
return false
|
||||
}
|
||||
if (this.provider.id === 'github' && isOpenAIo1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const stream = isSupportedStreamOutput()
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: messages as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
stream,
|
||||
keep_alive: this.keepAliveTime,
|
||||
temperature: assistant?.settings?.temperature
|
||||
})
|
||||
|
||||
return response.choices[0].message?.content || ''
|
||||
if (!stream) {
|
||||
return response.choices[0].message?.content || ''
|
||||
}
|
||||
|
||||
let text = ''
|
||||
|
||||
for await (const chunk of response) {
|
||||
text += chunk.choices[0]?.delta?.content || ''
|
||||
onResponse?.(text)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
|
||||
|
||||
@ -101,7 +101,13 @@ export async function fetchChatCompletion({
|
||||
return message
|
||||
}
|
||||
|
||||
export async function fetchTranslate({ message, assistant }: { message: Message; assistant: Assistant }) {
|
||||
interface FetchTranslateProps {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
onResponse?: (text: string) => void
|
||||
}
|
||||
|
||||
export async function fetchTranslate({ message, assistant, onResponse }: FetchTranslateProps) {
|
||||
const model = getTranslateModel()
|
||||
|
||||
if (!model) {
|
||||
@ -117,7 +123,7 @@ export async function fetchTranslate({ message, assistant }: { message: Message;
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
try {
|
||||
return await AI.translate(message, assistant)
|
||||
return await AI.translate(message, assistant, onResponse)
|
||||
} catch (error: any) {
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Messages from './Messages'
|
||||
import Messages from './components/Messages'
|
||||
|
||||
interface Props {
|
||||
route: string
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { uuid } from '@renderer/utils'
|
||||
@ -11,11 +13,11 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ChatWindow from '../chat/ChatWindow'
|
||||
import TranslateWindow from '../translate/TranslateWindow'
|
||||
import ClipboardPreview from './components/ClipboardPreview'
|
||||
import FeatureMenus from './components/FeatureMenus'
|
||||
import Footer from './components/Footer'
|
||||
import InputBar from './components/InputBar'
|
||||
import Translate from './Translate'
|
||||
|
||||
const HomeWindow: FC = () => {
|
||||
const [route, setRoute] = useState<'home' | 'chat' | 'translate' | 'summary' | 'explanation'>('home')
|
||||
@ -24,6 +26,7 @@ const HomeWindow: FC = () => {
|
||||
const [text, setText] = useState('')
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { defaultModel: model } = useDefaultModel()
|
||||
const { language } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const textRef = useRef(text)
|
||||
|
||||
@ -42,6 +45,10 @@ const HomeWindow: FC = () => {
|
||||
onReadClipboard()
|
||||
}, [onReadClipboard])
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || navigator.language || 'en-US')
|
||||
}, [language])
|
||||
|
||||
const onCloseWindow = () => isMiniWindow && window.close()
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@ -145,7 +152,7 @@ const HomeWindow: FC = () => {
|
||||
if (route === 'translate') {
|
||||
return (
|
||||
<Container>
|
||||
<Translate text={referenceText} />
|
||||
<TranslateWindow text={referenceText} />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Footer route={route} onExit={() => setRoute('home')} />
|
||||
</Container>
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { SwapOutlined } from '@ant-design/icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import db from '@renderer/databases'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { Assistant, Message } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||
import { Select, Space } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -14,70 +17,15 @@ interface Props {
|
||||
text: string
|
||||
}
|
||||
|
||||
const Translate: FC<Props> = ({ text }) => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState('')
|
||||
const [targetLanguage, setTargetLanguage] = useState('chinese')
|
||||
const { translateModel } = useDefaultModel()
|
||||
let _targetLanguage = 'chinese'
|
||||
|
||||
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 Translate: FC<Props> = ({ text }) => {
|
||||
const [result, setResult] = useState('')
|
||||
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
||||
const { translateModel } = useDefaultModel()
|
||||
const { t } = useTranslation()
|
||||
|
||||
_targetLanguage = targetLanguage
|
||||
|
||||
const translate = useCallback(async () => {
|
||||
if (!text.trim() || !translateModel) return
|
||||
@ -95,36 +43,41 @@ const Translate: FC<Props> = ({ text }) => {
|
||||
status: 'sending'
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const translateText = await fetchTranslate({ message, assistant })
|
||||
setResult(translateText)
|
||||
setLoading(false)
|
||||
await fetchTranslate({ message, assistant, onResponse: setResult })
|
||||
}, [text, targetLanguage, translateModel])
|
||||
|
||||
useEffect(() => {
|
||||
// 获取默认目标语言
|
||||
db.settings.get({ id: 'translate:target:language' }).then((targetLang) => {
|
||||
if (targetLang) {
|
||||
setTargetLanguage(targetLang.value)
|
||||
}
|
||||
runAsyncFunction(async () => {
|
||||
const targetLang = await db.settings.get({ id: 'translate:target:language' })
|
||||
targetLang && setTargetLanguage(targetLang.value)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
translate()
|
||||
}, [])
|
||||
}, [translate])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LanguageSelect>
|
||||
<MenuContainer>
|
||||
<Select
|
||||
value={targetLanguage}
|
||||
style={{ width: 140 }}
|
||||
showSearch
|
||||
value="any"
|
||||
style={{ width: 200 }}
|
||||
optionFilterProp="label"
|
||||
options={languageOptions}
|
||||
disabled
|
||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
||||
/>
|
||||
<SwapOutlined />
|
||||
<Select
|
||||
showSearch
|
||||
value={targetLanguage}
|
||||
style={{ width: 200 }}
|
||||
optionFilterProp="label"
|
||||
options={TranslateLanguageOptions}
|
||||
onChange={(value) => {
|
||||
setTargetLanguage(value)
|
||||
translate()
|
||||
db.settings.put({ id: 'translate:target:language', value })
|
||||
}}
|
||||
optionRender={(option) => (
|
||||
<Space>
|
||||
@ -135,8 +88,16 @@ const Translate: FC<Props> = ({ text }) => {
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
</LanguageSelect>
|
||||
<Main>{loading ? <LoadingText>翻译中...</LoadingText> : <ResultText>{result || text}</ResultText>}</Main>
|
||||
</MenuContainer>
|
||||
<Main>
|
||||
{isEmpty(result) ? (
|
||||
<LoadingText>{t('translate.output.placeholder')}...</LoadingText>
|
||||
) : (
|
||||
<OutputContainer>
|
||||
<ResultText>{result}</ResultText>
|
||||
</OutputContainer>
|
||||
)}
|
||||
</Main>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -158,7 +119,7 @@ const Main = styled.div`
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const ResultText = styled(Scrollbar)`
|
||||
const ResultText = styled.div`
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
width: 100%;
|
||||
@ -169,8 +130,19 @@ const LoadingText = styled.div`
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
const LanguageSelect = styled.div`
|
||||
margin-bottom: 8px;
|
||||
const MenuContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const OutputContainer = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
`
|
||||
|
||||
export default Translate
|
||||
Loading…
x
Reference in New Issue
Block a user