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 && ( +
+ ) + ); +} diff --git a/src/services/translate/ollama_custom/index.jsx b/src/services/translate/ollama_custom/index.jsx new file mode 100644 index 0000000000..cb10931acf --- /dev/null +++ b/src/services/translate/ollama_custom/index.jsx @@ -0,0 +1,54 @@ +import { store } from '../../../utils/store'; +import { Language } from './info'; +import { Ollama } from 'ollama/browser'; + +export async function translate(text, from, to, options = {}) { + const { config, setResult, detect } = options; + + let translateConfig = await store.get('ollama_custom'); + if (config !== undefined) { + translateConfig = config; + } + let { stream, promptList, requestPath, model } = translateConfig; + + if (!/https?:\/\/.+/.test(requestPath)) { + requestPath = `https://${requestPath}`; + } + if (requestPath.endsWith('/')) { + requestPath = requestPath.slice(0, -1); + } + const ollama = new Ollama({ host: requestPath }); + + promptList = promptList.map((item) => { + return { + ...item, + content: item.content + .replaceAll('$text', text) + .replaceAll('$from', from) + .replaceAll('$to', to) + .replaceAll('$detect', Language[detect]), + }; + }); + + const response = await ollama.chat({ model, messages: promptList, stream: stream }); + + if (stream) { + let target = ''; + for await (const part of response) { + target += part.message.content; + if (setResult) { + setResult(target + '_'); + } else { + ollama.abort(); + return '[STREAM]'; + } + } + setResult(target.trim()); + return target.trim(); + } else { + return response.message.content; + } +} + +export * from './Config'; +export * from './info'; diff --git a/src/services/translate/ollama_custom/info.ts b/src/services/translate/ollama_custom/info.ts new file mode 100644 index 0000000000..6001b76e37 --- /dev/null +++ b/src/services/translate/ollama_custom/info.ts @@ -0,0 +1,38 @@ +export const info = { + name: 'ollama_custom', + icon: 'logo/ollama.png', +}; + +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', +} diff --git a/src/services/translate/ollama_polish/Config.jsx b/src/services/translate/ollama_polish/Config.jsx new file mode 100644 index 0000000000..4304b0753e --- /dev/null +++ b/src/services/translate/ollama_polish/Config.jsx @@ -0,0 +1,339 @@ +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_polish', + { + stream: true, + model: 'gemma:2b', + requestPath: 'http://localhost:11434', + promptList: [ + { + role: 'system', + content: 'You are a text embellisher, you can only embellish the text, never interpret it.', + }, + { role: 'user', content: `Embellish in $detect:\n"""\n$text\n"""` }, + ], + }, + { 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(); + console.log(serviceConfig); + console.log(installedModels); + + 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 && ( + + ) + ); +} diff --git a/src/services/translate/ollama_polish/index.jsx b/src/services/translate/ollama_polish/index.jsx new file mode 100644 index 0000000000..89d3cc5a2f --- /dev/null +++ b/src/services/translate/ollama_polish/index.jsx @@ -0,0 +1,54 @@ +import { store } from '../../../utils/store'; +import { Language } from './info'; +import { Ollama } from 'ollama/browser'; + +export async function translate(text, from, to, options = {}) { + const { config, setResult, detect } = options; + + let translateConfig = await store.get('ollama_polish'); + if (config !== undefined) { + translateConfig = config; + } + let { stream, promptList, requestPath, model } = translateConfig; + + if (!/https?:\/\/.+/.test(requestPath)) { + requestPath = `https://${requestPath}`; + } + if (requestPath.endsWith('/')) { + requestPath = requestPath.slice(0, -1); + } + const ollama = new Ollama({ host: requestPath }); + + promptList = promptList.map((item) => { + return { + ...item, + content: item.content + .replaceAll('$text', text) + .replaceAll('$from', from) + .replaceAll('$to', to) + .replaceAll('$detect', Language[detect]), + }; + }); + + const response = await ollama.chat({ model, messages: promptList, stream: stream }); + + if (stream) { + let target = ''; + for await (const part of response) { + target += part.message.content; + if (setResult) { + setResult(target + '_'); + } else { + ollama.abort(); + return '[STREAM]'; + } + } + setResult(target.trim()); + return target.trim(); + } else { + return response.message.content; + } +} + +export * from './Config'; +export * from './info'; diff --git a/src/services/translate/ollama_polish/info.ts b/src/services/translate/ollama_polish/info.ts new file mode 100644 index 0000000000..31aaa26b81 --- /dev/null +++ b/src/services/translate/ollama_polish/info.ts @@ -0,0 +1,38 @@ +export const info = { + name: 'ollama_polish', + icon: 'logo/ollama.png', +}; + +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', +} diff --git a/src/services/translate/ollama_summary/Config.jsx b/src/services/translate/ollama_summary/Config.jsx new file mode 100644 index 0000000000..cfaf91cf55 --- /dev/null +++ b/src/services/translate/ollama_summary/Config.jsx @@ -0,0 +1,339 @@ +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_summary', + { + stream: true, + model: 'gemma:2b', + requestPath: 'http://localhost:11434', + promptList: [ + { + role: 'system', + content: 'You are a text summarizer, you can only summarize the text, never interpret it.', + }, + { role: 'user', content: `Summarize in $detect:\n"""\n$text\n"""` }, + ], + }, + { 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(); + console.log(serviceConfig); + console.log(installedModels); + + 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 && ( + + ) + ); +} diff --git a/src/services/translate/ollama_summary/index.jsx b/src/services/translate/ollama_summary/index.jsx new file mode 100644 index 0000000000..fa3f3e0cce --- /dev/null +++ b/src/services/translate/ollama_summary/index.jsx @@ -0,0 +1,54 @@ +import { store } from '../../../utils/store'; +import { Language } from './info'; +import { Ollama } from 'ollama/browser'; + +export async function translate(text, from, to, options = {}) { + const { config, setResult, detect } = options; + + let translateConfig = await store.get('ollama_summary'); + if (config !== undefined) { + translateConfig = config; + } + let { stream, promptList, requestPath, model } = translateConfig; + + if (!/https?:\/\/.+/.test(requestPath)) { + requestPath = `https://${requestPath}`; + } + if (requestPath.endsWith('/')) { + requestPath = requestPath.slice(0, -1); + } + const ollama = new Ollama({ host: requestPath }); + + promptList = promptList.map((item) => { + return { + ...item, + content: item.content + .replaceAll('$text', text) + .replaceAll('$from', from) + .replaceAll('$to', to) + .replaceAll('$detect', Language[detect]), + }; + }); + + const response = await ollama.chat({ model, messages: promptList, stream: stream }); + + if (stream) { + let target = ''; + for await (const part of response) { + target += part.message.content; + if (setResult) { + setResult(target + '_'); + } else { + ollama.abort(); + return '[STREAM]'; + } + } + setResult(target.trim()); + return target.trim(); + } else { + return response.message.content; + } +} + +export * from './Config'; +export * from './info'; diff --git a/src/services/translate/ollama_summary/info.ts b/src/services/translate/ollama_summary/info.ts new file mode 100644 index 0000000000..bf9ec2abd8 --- /dev/null +++ b/src/services/translate/ollama_summary/info.ts @@ -0,0 +1,38 @@ +export const info = { + name: 'ollama_summary', + icon: 'logo/ollama.png', +}; + +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', +}