-
-
Notifications
You must be signed in to change notification settings - Fork 509
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
1,472 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
Oops, something went wrong.