Skip to content

Commit

Permalink
feat: Support Gemini Pro
Browse files Browse the repository at this point in the history
  • Loading branch information
Pylogmon committed Dec 20, 2023
1 parent 0fcf744 commit 36bf586
Show file tree
Hide file tree
Showing 16 changed files with 1,472 additions and 0 deletions.
Binary file added public/logo/geminipro.webp
Binary file not shown.
16 changes: 16 additions & 0 deletions src/i18n/locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,22 @@
"openai_custom": {
"title": "OpenAI Custom"
},
"geminipro": {
"title": "Gemini Pro",
"api_key": "Api Key",
"stream": "Stream Mode",
"prompt_description": "By customizing the Prompt, you can customize the behavior of Gemini Pro. $text $from $to $detect will be replaced with the text to be translated, source language, target language and detected language.",
"add": "Add Prompt"
},
"geminipro_summary": {
"title": "Gemini Pro Summary"
},
"geminipro_polish": {
"title": "Gemini Pro Polish"
},
"geminipro_custom": {
"title": "Gemini Pro Custom"
},
"chatglm": {
"title": "ChatGLM",
"model": "Model",
Expand Down
16 changes: 16 additions & 0 deletions src/i18n/locales/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,22 @@
"prompt_description": "通过自定义Prompt自定义OpenAI行为, $text $from $to $detect 将会被替换为 待翻译文本,源语言,目标语言和检测到的语言。",
"add": "添加 Prompt"
},
"geminipro": {
"title": "Gemini Pro",
"api_key": "Api Key",
"stream": "流式输出",
"prompt_description": "通过自定义Prompt自定义Gemini Pro的行为, $text $from $to $detect 将会被替换为 待翻译文本,源语言,目标语言和检测到的语言。",
"add": "添加 Prompt"
},
"geminipro_summary": {
"title": "Gemini Pro 总结"
},
"geminipro_polish": {
"title": "Gemini Pro 润色"
},
"geminipro_custom": {
"title": "Gemini Pro 自定义"
},
"chatglm": {
"title": "智谱 AI",
"model": "模型",
Expand Down
222 changes: 222 additions & 0 deletions src/services/translate/geminipro/Config.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { Input, Button, Switch, Textarea } 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, { useState } from 'react';

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(
'geminipro',
{
stream: true,
apiKey: '',
promptList: [
{
role: 'user',
parts: [
{
text: '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: 'model',
parts: [
{
text: 'Ok, I will only translate the text content, never interpret it.',
},
],
},
{
role: 'user',
parts: [
{
text: `Translate into Chinese\n"""\nhello\n"""`,
},
],
},
{
role: 'model',
parts: [
{
text: '你好',
},
],
},
{
role: 'user',
parts: [
{
text: `Translate into $to\n"""\n$text\n"""`,
},
],
},
],
},
{ sync: false }
);
const [isLoading, setIsLoading] = useState(false);

const { t } = useTranslation();
const toastStyle = useToastStyle();

return (
serviceConfig !== null && (
<form
onSubmit={(e) => {
e.preventDefault();
setIsLoading(true);
translate('hello', Language.auto, Language.zh_cn, { config: serviceConfig }).then(
() => {
setIsLoading(false);
setServiceConfig(serviceConfig, true);
updateServiceList('geminipro');
onClose();
},
(e) => {
setIsLoading(false);
toast.error(t('config.service.test_failed') + e.toString(), { style: toastStyle });
}
);
}}
>
<Toaster />
<div className='config-item'>
<h3 className='my-auto'>{t('services.help')}</h3>
<Button
onPress={() => {
open('https://pot-app.com/docs/api/translate/geminipro.html');
}}
>
{t('services.help')}
</Button>
</div>
<div className='config-item'>
<Switch
isSelected={serviceConfig['stream']}
onValueChange={(value) => {
setServiceConfig({
...serviceConfig,
stream: value,
});
}}
classNames={{
base: 'flex flex-row-reverse justify-between w-full max-w-full',
}}
>
{t('services.translate.geminipro.stream')}
</Switch>
</div>
<div className='config-item'>
<Input
label={t('services.translate.geminipro.api_key')}
labelPlacement='outside-left'
type='password'
value={serviceConfig['apiKey']}
variant='bordered'
classNames={{
base: 'justify-between',
label: 'text-[length:--nextui-font-size-medium]',
mainWrapper: 'max-w-[50%]',
}}
onValueChange={(value) => {
setServiceConfig({
...serviceConfig,
apiKey: value,
});
}}
/>
</div>
<h3 className='my-auto'>Prompt List</h3>
<p className='text-[10px] text-default-700'>{t('services.translate.geminipro.prompt_description')}</p>

<div className='bg-content2 rounded-[10px] p-3'>
{serviceConfig.promptList &&
serviceConfig.promptList.map((prompt, index) => {
return (
<div className='config-item'>
<Textarea
label={prompt.role}
labelPlacement='outside'
variant='faded'
value={prompt.parts[0].text}
placeholder={`Input Some ${prompt.role} Prompt`}
onValueChange={(value) => {
setServiceConfig({
...serviceConfig,
promptList: serviceConfig.promptList.map((p, i) => {
if (i === index) {
return {
role: index % 2 !== 0 ? 'model' : 'user',
parts: [
{
text: value,
},
],
};
} else {
return p;
}
}),
});
}}
/>
<Button
isIconOnly
color='danger'
className='my-auto mx-1'
variant='flat'
onPress={() => {
setServiceConfig({
...serviceConfig,
promptList: serviceConfig.promptList.filter((p, i) => i !== index),
});
}}
>
<MdDeleteOutline className='text-[18px]' />
</Button>
</div>
);
})}
<Button
fullWidth
onPress={() => {
setServiceConfig({
...serviceConfig,
promptList: [
...serviceConfig.promptList,
{
role: serviceConfig.promptList.length % 2 === 0 ? 'user' : 'model',
parts: [
{
text: '',
},
],
},
],
});
}}
>
{t('services.translate.geminipro.add')}
</Button>
</div>
<br />
<Button
type='submit'
isLoading={isLoading}
fullWidth
color='primary'
>
{t('common.save')}
</Button>
</form>
)
);
}
115 changes: 115 additions & 0 deletions src/services/translate/geminipro/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { fetch, Body } from '@tauri-apps/api/http';
import { store } from '../../../utils/store';
import { Language } from './info';

export async function translate(text, from, to, options = {}) {
const { config, setResult, detect } = options;

let translateConfig = await store.get('geminipro');
if (config !== undefined) {
translateConfig = config;
}
let { apiKey, stream, promptList } = translateConfig;

const requestPath = stream
? `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:streamGenerateContent?key=${apiKey}`
: `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`;

promptList = promptList.map((item) => {
return {
...item,
parts: [
{
text: item.parts[0].text
.replaceAll('$text', text)
.replaceAll('$from', from)
.replaceAll('$to', to)
.replaceAll('$detect', Language[detect]),
},
],
};
});

const headers = {
'Content-Type': 'application/json',
};
let body = {
contents: promptList,
};

if (stream) {
const res = await window.fetch(requestPath, {
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).replaceAll(/\s+/g, '');
const matchs = str.match(/{\"text\":\".*\"}],/);
if (matchs.length > 0) {
for (let match of matchs) {
let result = JSON.parse(match.slice(0, -2));
if (result.text) {
target += result.text;
if (setResult) {
setResult(target + '_');
} else {
return '[STREAM]';
}
}
}
temp = '';
} else {
temp += str;
}
}
} finally {
reader.releaseLock();
}
} else {
throw `Http Request Error\nHttp Status: ${res.status}\n${JSON.stringify(res.data)}`;
}
} else {
let res = await fetch(requestPath, {
method: 'POST',
headers: headers,
body: Body.json(body),
});

if (res.ok) {
let result = res.data;
const { candidates } = result;
if (candidates) {
let target = candidates[0].content.parts[0].text.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(candidates);
}
} 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';
Loading

0 comments on commit 36bf586

Please sign in to comment.