diff --git a/src/i18n/locales/en_US.json b/src/i18n/locales/en_US.json index b12ea7800b..d0dccaca57 100644 --- a/src/i18n/locales/en_US.json +++ b/src/i18n/locales/en_US.json @@ -255,6 +255,15 @@ "prompt_description": "By customizing the Prompt, you can customize the behavior of AI. $text $from $to $detect will be replaced with the text to be translated, source language, target language and detected language.", "add": "Add Prompt" }, + "ollama_polish": { + "title": "Ollama Polish" + }, + "ollama_summary": { + "title": "Ollama Summary" + }, + "ollama_custom": { + "title": "Ollama Custom" + }, "openai": { "title": "OpenAI", "service": "Service Provider", diff --git a/src/i18n/locales/zh_CN.json b/src/i18n/locales/zh_CN.json index b23917a257..b4d4cb19f6 100644 --- a/src/i18n/locales/zh_CN.json +++ b/src/i18n/locales/zh_CN.json @@ -255,6 +255,15 @@ "prompt_description": "通过自定义Prompt自定义AI的行为, $text $from $to $detect 将会被替换为 待翻译文本,源语言,目标语言和检测到的语言。", "add": "添加 Prompt" }, + "ollama_polish": { + "title": "Ollama 润色" + }, + "ollama_summary": { + "title": "Ollama 总结" + }, + "ollama_custom": { + "title": "Ollama 自定义" + }, "openai": { "title": "OpenAI", "service": "服务提供商", diff --git a/src/services/translate/index.jsx b/src/services/translate/index.jsx index de0d4b5aba..325ee05f59 100644 --- a/src/services/translate/index.jsx +++ b/src/services/translate/index.jsx @@ -26,6 +26,9 @@ import * as _geminipro_summary from './geminipro_summary'; import * as _geminipro_polish from './geminipro_polish'; import * as _geminipro_custom from './geminipro_custom'; import * as _ollama from './ollama'; +import * as _ollama_summary from './ollama_summary'; +import * as _ollama_polish from './ollama_polish'; +import * as _ollama_custom from './ollama_custom'; export const deepl = _deepl; export const bing = _bing; @@ -55,3 +58,6 @@ export const geminipro_summary = _geminipro_summary; export const geminipro_polish = _geminipro_polish; export const geminipro_custom = _geminipro_custom; export const ollama = _ollama; +export const ollama_summary = _ollama_summary; +export const ollama_polish = _ollama_polish; +export const ollama_custom = _ollama_custom; diff --git a/src/services/translate/ollama/Config.jsx b/src/services/translate/ollama/Config.jsx index 38a76b2e39..e6bc1c7f29 100644 --- a/src/services/translate/ollama/Config.jsx +++ b/src/services/translate/ollama/Config.jsx @@ -4,7 +4,7 @@ import toast, { Toaster } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { open } from '@tauri-apps/api/shell'; import React, { useEffect, useState } from 'react'; -import { Ollama } from 'ollama'; +import { Ollama } from 'ollama/browser'; import { useConfig } from '../../../hooks/useConfig'; import { useToastStyle } from '../../../hooks'; diff --git a/src/services/translate/ollama/index.jsx b/src/services/translate/ollama/index.jsx index 79ce5b1976..07293c6af0 100644 --- a/src/services/translate/ollama/index.jsx +++ b/src/services/translate/ollama/index.jsx @@ -1,7 +1,6 @@ -import { fetch, Body } from '@tauri-apps/api/http'; import { store } from '../../../utils/store'; import { Language } from './info'; -import { Ollama } from 'ollama'; +import { Ollama } from 'ollama/browser'; export async function translate(text, from, to, options = {}) { const { config, setResult, detect } = options; diff --git a/src/services/translate/ollama_custom/Config.jsx b/src/services/translate/ollama_custom/Config.jsx new file mode 100644 index 0000000000..e6b741ac53 --- /dev/null +++ b/src/services/translate/ollama_custom/Config.jsx @@ -0,0 +1,337 @@ +import { Input, Button, Switch, Textarea, Card, CardBody, Link, Tooltip, Progress } from '@nextui-org/react'; +import { MdDeleteOutline } from 'react-icons/md'; +import toast, { Toaster } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { open } from '@tauri-apps/api/shell'; +import React, { useEffect, useState } from 'react'; +import { Ollama } from 'ollama/browser'; + +import { useConfig } from '../../../hooks/useConfig'; +import { useToastStyle } from '../../../hooks'; +import { translate } from './index'; +import { Language } from './index'; + +export function Config(props) { + const { updateServiceList, onClose } = props; + const [serviceConfig, setServiceConfig] = useConfig( + 'ollama_custom', + { + stream: true, + model: 'gemma:2b', + requestPath: 'http://localhost:11434', + promptList: [ + { + role: 'system', + content: 'You are a helpful assistant.', + }, + { role: 'user', content: '$text' }, + ], + }, + { sync: false } + ); + const [isLoading, setIsLoading] = useState(false); + const [isPulling, setIsPulling] = useState(false); + const [progress, setProgress] = useState(0); + const [pullingStatus, setPullingStatus] = useState(''); + const [installedModels, setInstalledModels] = useState(null); + const { t } = useTranslation(); + const toastStyle = useToastStyle(); + + async function getModles() { + try { + const ollama = new Ollama({ host: serviceConfig.requestPath }); + const list = await ollama.list(); + setInstalledModels(list); + } catch { + setInstalledModels(null); + } + } + + async function pullModel() { + setIsPulling(true); + const ollama = new Ollama({ host: serviceConfig.requestPath }); + const stream = await ollama.pull({ model: serviceConfig.model, stream: true }); + for await (const part of stream) { + console.log(part); + if (part.digest) { + let percent = 0; + if (part.completed && part.total) { + percent = Math.round((part.completed / part.total) * 100); + } + setProgress(percent); + setPullingStatus(part.status); + } else { + setProgress(0); + setPullingStatus(part.status); + } + } + setProgress(0); + setPullingStatus(''); + setIsPulling(false); + getModles(); + } + + useEffect(() => { + if (serviceConfig !== null) { + getModles(); + } + }, [serviceConfig]); + + return ( + serviceConfig !== null && ( +
{ + e.preventDefault(); + setIsLoading(true); + translate('hello', Language.auto, Language.zh_cn, { config: serviceConfig }).then( + () => { + setIsLoading(false); + setServiceConfig(serviceConfig, true); + updateServiceList('ollama_custom'); + onClose(); + }, + (e) => { + setIsLoading(false); + toast.error(t('config.service.test_failed') + e.toString(), { style: toastStyle }); + } + ); + }} + > + + {installedModels === null && ( + + +
+ {t('services.translate.ollama.install_ollama')} +
+ + {t('services.translate.ollama.install_ollama_link')} + +
+
+
+ )} +
+

{t('services.help')}

+ +
+
+ { + setServiceConfig({ + ...serviceConfig, + stream: value, + }); + }} + classNames={{ + base: 'flex flex-row-reverse justify-between w-full max-w-full', + }} + > + {t('services.translate.ollama.stream')} + +
+
+ { + setServiceConfig({ + ...serviceConfig, + requestPath: value, + }); + }} + /> +
+
+ { + setServiceConfig({ + ...serviceConfig, + model: value, + }); + }} + endContent={ + installedModels && + !installedModels.models + .map((model) => { + return model.name; + }) + .includes(serviceConfig['model']) ? ( + + + + ) : ( + + ) + } + /> +
+ + + {isPulling && ( + + )} +
+ + {t('services.translate.ollama.supported_models')} + +
+
+
+

Prompt List

+

{t('services.translate.ollama.prompt_description')}

+ +
+ {serviceConfig.promptList && + serviceConfig.promptList.map((prompt, index) => { + return ( +
+