diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts new file mode 100644 index 00000000..ee5c2acf --- /dev/null +++ b/src/renderer/src/config/translate.ts @@ -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: '🇸🇦' + } +] diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index af5c8622..801e9957 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -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 }) diff --git a/src/renderer/src/providers/AiProvider.ts b/src/renderer/src/providers/AiProvider.ts index 169eb7a0..e0e84db3 100644 --- a/src/renderer/src/providers/AiProvider.ts +++ b/src/renderer/src/providers/AiProvider.ts @@ -20,8 +20,8 @@ export default class AiProvider { return this.sdk.completions({ messages, assistant, onChunk, onFilterMessages }) } - public async translate(message: Message, assistant: Assistant): Promise { - return this.sdk.translate(message, assistant) + public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void): Promise { + return this.sdk.translate(message, assistant, onResponse) } public async summaries(messages: Message[], assistant: Assistant): Promise { diff --git a/src/renderer/src/providers/AnthropicProvider.ts b/src/renderer/src/providers/AnthropicProvider.ts index 3ceae8a4..c13b0bd7 100644 --- a/src/renderer/src/providers/AnthropicProvider.ts +++ b/src/renderer/src/providers/AnthropicProvider.ts @@ -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((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 { diff --git a/src/renderer/src/providers/BaseProvider.ts b/src/renderer/src/providers/BaseProvider.ts index 4c12382a..29cdb5c2 100644 --- a/src/renderer/src/providers/BaseProvider.ts +++ b/src/renderer/src/providers/BaseProvider.ts @@ -20,7 +20,7 @@ export default abstract class BaseProvider { } abstract completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise - abstract translate(message: Message, assistant: Assistant): Promise + abstract translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void): Promise abstract summaries(messages: Message[], assistant: Assistant): Promise abstract suggestions(messages: Message[], assistant: Assistant): Promise abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise diff --git a/src/renderer/src/providers/GeminiProvider.ts b/src/renderer/src/providers/GeminiProvider.ts index 6b30d712..e16ed522 100644 --- a/src/renderer/src/providers/GeminiProvider.ts +++ b/src/renderer/src/providers/GeminiProvider.ts @@ -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 { diff --git a/src/renderer/src/providers/OpenAIProvider.ts b/src/renderer/src/providers/OpenAIProvider.ts index 87cbfcf8..84e0005e 100644 --- a/src/renderer/src/providers/OpenAIProvider.ts +++ b/src/renderer/src/providers/OpenAIProvider.ts @@ -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 { diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 22859245..47ab6ffc 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -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 '' } diff --git a/src/renderer/src/windows/mini/chat/ChatWindow.tsx b/src/renderer/src/windows/mini/chat/ChatWindow.tsx index f410b832..3efdafd4 100644 --- a/src/renderer/src/windows/mini/chat/ChatWindow.tsx +++ b/src/renderer/src/windows/mini/chat/ChatWindow.tsx @@ -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 diff --git a/src/renderer/src/windows/mini/chat/Inputbar.tsx b/src/renderer/src/windows/mini/chat/components/Inputbar.tsx similarity index 100% rename from src/renderer/src/windows/mini/chat/Inputbar.tsx rename to src/renderer/src/windows/mini/chat/components/Inputbar.tsx diff --git a/src/renderer/src/windows/mini/chat/Message.tsx b/src/renderer/src/windows/mini/chat/components/Message.tsx similarity index 100% rename from src/renderer/src/windows/mini/chat/Message.tsx rename to src/renderer/src/windows/mini/chat/components/Message.tsx diff --git a/src/renderer/src/windows/mini/chat/Messages.tsx b/src/renderer/src/windows/mini/chat/components/Messages.tsx similarity index 100% rename from src/renderer/src/windows/mini/chat/Messages.tsx rename to src/renderer/src/windows/mini/chat/components/Messages.tsx diff --git a/src/renderer/src/windows/mini/home/HomeWindow.tsx b/src/renderer/src/windows/mini/home/HomeWindow.tsx index 42865749..081534cf 100644 --- a/src/renderer/src/windows/mini/home/HomeWindow.tsx +++ b/src/renderer/src/windows/mini/home/HomeWindow.tsx @@ -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) => { @@ -145,7 +152,7 @@ const HomeWindow: FC = () => { if (route === 'translate') { return ( - +