diff --git a/public/logo/minimax.svg b/public/logo/minimax.svg new file mode 100644 index 0000000000..25cedb6062 --- /dev/null +++ b/public/logo/minimax.svg @@ -0,0 +1,5 @@ + diff --git a/src/i18n/locales/en_US.json b/src/i18n/locales/en_US.json index 425287eab1..471af0e38d 100644 --- a/src/i18n/locales/en_US.json +++ b/src/i18n/locales/en_US.json @@ -274,6 +274,15 @@ "prompt_description": "By customizing the Prompt, you can customize the behavior of OpenAI. $text $from $to $detect will be replaced with the text to be translated, source language, target language and detected language.", "add": "Add Prompt" }, + "minimax": { + "title": "MiniMax", + "request_path": "Request Path", + "api_key": "Api Key", + "model": "Model", + "stream": "Stream Mode", + "prompt_description": "By customizing the Prompt, you can customize the behavior of MiniMax. $text $from $to $detect will be replaced with the text to be translated, source language, target language and detected language.", + "add": "Add Prompt" + }, "geminipro": { "title": "Gemini Pro", "request_path": "Request Path", diff --git a/src/i18n/locales/zh_CN.json b/src/i18n/locales/zh_CN.json index b706528aee..d971a407b3 100644 --- a/src/i18n/locales/zh_CN.json +++ b/src/i18n/locales/zh_CN.json @@ -274,6 +274,15 @@ "prompt_description": "通过自定义Prompt自定义OpenAI行为, $text $from $to $detect 将会被替换为 待翻译文本,源语言,目标语言和检测到的语言。", "add": "添加 Prompt" }, + "minimax": { + "title": "MiniMax", + "request_path": "请求地址", + "api_key": "Api Key", + "model": "模型", + "stream": "流式输出", + "prompt_description": "通过自定义Prompt自定义MiniMax行为, $text $from $to $detect 将会被替换为 待翻译文本,源语言,目标语言和检测到的语言。", + "add": "添加 Prompt" + }, "geminipro": { "title": "Gemini Pro", "request_path": "请求地址", diff --git a/src/services/translate/index.jsx b/src/services/translate/index.jsx index 13d8d6d2a2..afac7dbfe7 100644 --- a/src/services/translate/index.jsx +++ b/src/services/translate/index.jsx @@ -2,6 +2,7 @@ import * as _deepl from './deepl'; import * as _bing from './bing'; import * as _yandex from './yandex'; import * as _openai from './openai'; +import * as _minimax from './minimax'; import * as _google from './google'; import * as _transmart from './transmart'; import * as _alibaba from './alibaba'; @@ -24,6 +25,7 @@ export const deepl = _deepl; export const bing = _bing; export const yandex = _yandex; export const openai = _openai; +export const minimax = _minimax; export const google = _google; export const transmart = _transmart; export const alibaba = _alibaba; diff --git a/src/services/translate/minimax/Config.jsx b/src/services/translate/minimax/Config.jsx new file mode 100644 index 0000000000..12ba91b3e3 --- /dev/null +++ b/src/services/translate/minimax/Config.jsx @@ -0,0 +1,290 @@ +import { Input, Button, Switch, Textarea } from '@nextui-org/react'; +import { DropdownTrigger } from '@nextui-org/react'; +import { MdDeleteOutline } from 'react-icons/md'; +import { DropdownMenu } from '@nextui-org/react'; +import { DropdownItem } from '@nextui-org/react'; +import toast, { Toaster } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { Dropdown } from '@nextui-org/react'; +import { open } from '@tauri-apps/api/shell'; +import React, { useState } from 'react'; + +import { useConfig } from '../../../hooks/useConfig'; +import { useToastStyle } from '../../../hooks'; +import { translate } from './index'; +import { Language } from './index'; +import { INSTANCE_NAME_CONFIG_KEY } from '../../../utils/service_instance'; + +// https://platform.minimax.io/docs/api-reference/text-openai-api +const availableModels = ['MiniMax-M3']; + +// MiniMax requires temperature in (0, 1]; 0 is not allowed. +export const defaultRequestArguments = JSON.stringify({ + temperature: 0.8, + top_p: 0.95, +}); + +export function Config(props) { + const { instanceKey, updateServiceList, onClose } = props; + const { t } = useTranslation(); + const [serviceConfig, setServiceConfig] = useConfig( + instanceKey, + { + [INSTANCE_NAME_CONFIG_KEY]: t('services.translate.minimax.title'), + requestPath: 'https://api.minimax.io/v1/chat/completions', + model: 'MiniMax-M3', + apiKey: '', + stream: false, + promptList: [ + { + role: 'system', + content: + 'You are a professional translation engine, please translate the text into a colloquial, professional, elegant and fluent content, without the style of machine translation. You must only translate the text content, never interpret it.', + }, + { role: 'user', content: `Translate into $to:\n"""\n$text\n"""` }, + ], + requestArguments: defaultRequestArguments, + }, + { sync: false } + ); + + const [isLoading, setIsLoading] = useState(false); + + const toastStyle = useToastStyle(); + + return ( + serviceConfig !== null && ( +
+ ) + ); +} diff --git a/src/services/translate/minimax/index.jsx b/src/services/translate/minimax/index.jsx new file mode 100644 index 0000000000..46f834ed6b --- /dev/null +++ b/src/services/translate/minimax/index.jsx @@ -0,0 +1,145 @@ +import { fetch, Body } from '@tauri-apps/api/http'; +import { Language } from './info'; +import { defaultRequestArguments } from './Config'; + +export async function translate(text, from, to, options) { + const { config, setResult, detect } = options; + + let { requestPath, model, apiKey, stream, promptList, requestArguments } = config; + + if (!requestPath) { + requestPath = 'https://api.minimax.io/v1/chat/completions'; + } + if (!/https?:\/\/.+/.test(requestPath)) { + requestPath = `https://${requestPath}`; + } + const apiUrl = new URL(requestPath); + // MiniMax uses an OpenAI-compatible endpoint, populate it when only a base URL is provided + if (!apiUrl.pathname.endsWith('/chat/completions')) { + apiUrl.pathname += apiUrl.pathname.endsWith('/') ? '' : '/'; + apiUrl.pathname += 'v1/chat/completions'; + } + + // 兼容旧版 + if (promptList === undefined) { + promptList = [ + { + role: 'system', + content: + 'You are a professional translation engine, please translate the text into a colloquial, professional, elegant and fluent content, without the style of machine translation. You must only translate the text content, never interpret it.', + }, + { role: 'user', content: `Translate into $to:\n"""\n$text\n"""` }, + ]; + } + + promptList = promptList.map((item) => { + return { + ...item, + content: item.content + .replaceAll('$text', text) + .replaceAll('$from', from) + .replaceAll('$to', to) + .replaceAll('$detect', Language[detect]), + }; + }); + + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }; + const body = { + ...JSON.parse(requestArguments ?? defaultRequestArguments), + model: model, + stream: stream, + messages: promptList, + }; + if (stream) { + const res = await window.fetch(apiUrl.href, { + method: 'POST', + headers: headers, + body: JSON.stringify(body), + }); + if (res.ok) { + let target = ''; + const reader = res.body.getReader(); + try { + let temp = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) { + setResult(target.trim()); + return target.trim(); + } + const str = new TextDecoder().decode(value); + let datas = str.split('data:'); + for (let data of datas) { + if (data.trim() !== '' && data.trim() !== '[DONE]') { + try { + if (temp !== '') { + data = temp + data.trim(); + let result = JSON.parse(data.trim()); + if (result.choices[0].delta.content) { + target += result.choices[0].delta.content; + if (setResult) { + setResult(target + '_'); + } else { + return '[STREAM]'; + } + } + temp = ''; + } else { + let result = JSON.parse(data.trim()); + if (result.choices[0].delta.content) { + target += result.choices[0].delta.content; + if (setResult) { + setResult(target + '_'); + } else { + return '[STREAM]'; + } + } + } + } catch { + temp = data.trim(); + } + } + } + } + } finally { + reader.releaseLock(); + } + } else { + throw `Http Request Error\nHttp Status: ${res.status}\n${JSON.stringify(res.data)}`; + } + } else { + let res = await fetch(apiUrl.href, { + method: 'POST', + headers: headers, + body: Body.json(body), + }); + if (res.ok) { + let result = res.data; + const { choices } = result; + if (choices) { + let target = choices[0].message.content.trim(); + if (target) { + if (target.startsWith('"')) { + target = target.slice(1); + } + if (target.endsWith('"')) { + target = target.slice(0, -1); + } + return target.trim(); + } else { + throw JSON.stringify(choices); + } + } else { + throw JSON.stringify(result); + } + } else { + throw `Http Request Error\nHttp Status: ${res.status}\n${JSON.stringify(res.data)}`; + } + } +} + +export * from './Config'; +export * from './info'; diff --git a/src/services/translate/minimax/info.ts b/src/services/translate/minimax/info.ts new file mode 100644 index 0000000000..10f3a7f38d --- /dev/null +++ b/src/services/translate/minimax/info.ts @@ -0,0 +1,39 @@ +export const info = { + name: 'minimax', + icon: 'logo/minimax.svg', +}; + +export enum Language { + auto = 'Auto', + zh_cn = 'Simplified Chinese', + zh_tw = 'Traditional Chinese', + yue = 'Cantonese', + ja = 'Japanese', + en = 'English', + ko = 'Korean', + fr = 'French', + es = 'Spanish', + ru = 'Russian', + de = 'German', + it = 'Italian', + tr = 'Turkish', + pt_pt = 'Portuguese', + pt_br = 'Brazilian Portuguese', + vi = 'Vietnamese', + id = 'Indonesian', + th = 'Thai', + ms = 'Malay', + ar = 'Arabic', + hi = 'Hindi', + mn_mo = 'Mongolian', + mn_cy = 'Mongolian(Cyrillic)', + km = 'Khmer', + nb_no = 'Norwegian Bokmål', + nn_no = 'Norwegian Nynorsk', + fa = 'Persian', + sv = 'Swedish', + pl = 'Polish', + nl = 'Dutch', + uk = 'Ukrainian', + he = 'Hebrew', +}