Former-commit-id: e255ced1c91ef6a7722dce7268872be9ee326168
This commit is contained in:
Physton
2023-05-08 20:35:02 +08:00
parent 24c08a31cf
commit fa091e3dc1
60 changed files with 5566 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/.idea
/development_styles
/index.php
/src/node_modules
/src/package-lock.json

185
README.MD Normal file
View File

@@ -0,0 +1,185 @@
# sd-webui-prompt-all-in-one
------------
sd-webui-prompt-all-in-one is an extension based on stable-diffusion-webui that aims to improve the user experience of the prompt/negative prompt input box. It has a more intuitive and powerful input interface, provides automatic translation, history and collection functions, and supports multiple languages to meet the needs of different users.
*sd-webui-prompt-all-in-one 是一个基于 stable-diffusion-webui
的扩展,旨在提高提示词/反向提示词输入框的使用体验。它拥有更直观、强大的输入界面功能,它提供了自动翻译、历史记录和收藏等功能,它支持多种语言,满足不同用户的需求。*
> If you find this extension helpful, please give me a star on Github!
> You could also buy me a coffee: [donate](#donate)
> 如果你觉得这个扩展有帮助请在Github上给我一颗星
> 你也可以请我喝杯咖啡: [donate](#donate)
## Installation and Usage / *安装使用*
------------
### Method 1 (Recommended) / *方式一(推荐)*
1. Open the terminal and enter your stable-diffusion-webui directory.
*打开终端,进入到你的 stable-diffusion-webui 目录下。*
For example / *例如*
`cd "C:\stable-diffusion-webui"`
2. Use git to clone sd-webui-prompt-all-in-one to the extensions directory of stable-diffusion-webui.
*使用 git 克隆 sd-webui-prompt-all-in-one 到 stable-diffusion-webui 的 extensions 目录下。*
```shell
git clone "https://github.com/Physton/sd-webui-prompt-all-in-one.git" extensions/sd-webui-prompt-all-in-one
```
3. Restart stable-diffusion-webui.
*重启 stable-diffusion-webui。*
### Method 2 / *方式二*
1. Download the source code package of [sd-webui-prompt-all-in-one](https://github.com/physton/sd-webui-prompt-all-in-one/releases), and unzip it to the extensions directory of stable-diffusion-webui.
*下载 [sd-webui-prompt-all-in-one](https://github.com/physton/sd-webui-prompt-all-in-one/releases) 的源码压缩包,解压到 stable-diffusion-webui 的 extensions 目录下。*
2. Restart stable-diffusion-webui.
*重启 stable-diffusion-webui。*
## Features / *功能特性*
------------
- More intuitive and powerful prompt input interface: supports adding new keywords with one click, drag and drop sorting, one-click copying and one-click translation. At the same time, the interface can display the translation results of English<->local language intuitively.
*更直观、强大的 prompt 输入界面:支持回车添加新关键词、拖动排序、一键复制和一键翻译。同时,界面中能够直观地显示英文<->本地语言的翻译结果。*
- Automatic translation: automatically translate the local language into English, and translate the English input of the prompt into the local language at the same time.
*自动翻译:自动将本地语言翻译成英文,同时将 prompt 输入的英文翻译成本地语言。*
- History and collection: support recording the history and collection of the prompt, making it convenient for users to quickly search and use.
*历史记录和收藏:支持记录 prompt 的历史记录和收藏,方便用户快速查找和使用。*
- Multi-language support: currently supports Simplified Chinese, Traditional Chinese, English, Russian, Japanese, Korean, French, German, Spanish, Portuguese, Italian, and Spanish, meeting the needs of different users.
*多国语言支持:目前支持简体中文、繁体中文、英文、俄语、日语、韩语、法语、德语、西班牙语、葡萄牙语、意大利语、西班牙语,满足不同用户的需求。*
## Demo / *演示*
------------
- Automatic translation / *自动翻译*
![](images/auto_translate.gif)
- One-click translation / *一键翻译*
![](images/translate.gif)
- Feature 1 / *特性1*
![](images/feature1.gif)
- Feature 2 / *特性2*
![](images/feature2.gif)
## Contributing / *帮助开发*
------------
If you want to contribute to sd-webui-prompt-all-in-one, welcome to submit issues and pull requests. You can also
contact me via:
*如果你想为 sd-webui-prompt-all-in-one 做出贡献,欢迎提交 issue 和 pull request。同时你也可以通过以下方式联系我*
- Email / *邮箱*physton@163.com
- Wechat / *微信*physton8
- QQ群820700336
------------
## Development / *开发方式*
- Front-end development (vue) / *前端开发vue*
1. Enter the src directory / *进入 src 目录*
`cd ./sd-webui-prompt-all-in-one/src`
2. Initialize the node environment / *初始化node环境*
`npm install`
3. Modify the code / *修改代码*
4. Compile / *编译*
`npm run build`
5. Restart stable-diffusion-webui / *重启 stable-diffusion-webui。*
- Back-end development (python) / *后端开发python*
1. Enter the extension directory / *进入扩展目录*`cd ./sd-webui-prompt-all-in-one`
2. Modify the code / *修改代码*
3. Restart stable-diffusion-webui / *重启 stable-diffusion-webui。*
------------
## Future / *未来计划*
- Support more languages / *支持更多的语言*
- Optimize translation services / *优化翻译服务*
- More powerful history function / *更强大的历史记录功能*
- ......
------------
## FAQ / *常见问题列表*
### Q: What languages does sd-webui-prompt-all-in-one support?
A: sd-webui-prompt-all-in-one supports multiple languages, including Simplified Chinese, Traditional Chinese, English, Russian, Japanese, Korean, French, German, Spanish, Portuguese, Italian, and Spanish.
### Q: What is the difference between translation interfaces that do not require API keys and those that require API keys?
A: The translation interface that does not require an API key is obtained through crawling and does not require the user to apply for an API key. However, there may be translation failures, instability, slow speed, and lack of support for concurrency. The translation interface that requires an API key calls the API interface of a third-party translation service and requires the user to apply for an API key. However, the translation success rate is relatively high, some interfaces support high concurrency, and the translation speed is fast.
### Q: sd-webui-prompt-all-in-one 支持哪些语言?
A: sd-webui-prompt-all-in-one 支持多种语言,包括简体中文、繁体中文、英文、俄语、日语、韩语、法语、德语、西班牙语、葡萄牙语、意大利语、西班牙语。
### Q: 无需api key的翻译接口和需要api key的翻译接口有什么区别
A: 无需api key的翻译接口是通过爬虫获取的不需要用户自己申请api key但是可能会存在翻译失败的情况稳定下差速度慢不支持并发。需要api key的翻译接口是通过调用第三方翻译服务的api接口需要用户自己申请api key但是翻译成功率较高某些接口支持高并发翻译速度快。
## Donate
------------
### Buy me a coffee / *请我喝杯咖啡*
#### Click / *点击* 👉 [![](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/physton/donate)
#### BTC-Bitcoin `3LdXwHkkVsspQzgowdy3pymwty9LiFBHda`
#### ETH-Ethereum `3DbapbGRQdbrDqBazBPYWfyKjbM5uN2SDy`
#### USDT-ERC20 `0xb8b7a067c5639e7befb9665be9bd00a4c75b6614`
#### USDT-TRC20 `TWtnKQgSTpHu1eiQGrppSniGyzYtggZYFf`
#### Paypal `physton@163.com`
#### Alipay / 支付宝 `physton@163.com`
#### Wechat / 微信赞赏
![](images/donate-wechat.jpg)
Developed by: https://www.physton.com

1
i18n.json.REMOVED.git-id Normal file
View File

@@ -0,0 +1 @@
92e45aa76d6df2b0138eb9a37a2d9b0ab5063c47

View File

@@ -0,0 +1 @@
e6eb04dc8666f28667c969793ba40451dc18fe91

BIN
images/donate-wechat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1 @@
f3574209133944e214859edf91d9927387ab2ea2

View File

@@ -0,0 +1 @@
44c5783e3571137accc6dbbd2420115101738dff

View File

@@ -0,0 +1 @@
bac9e6484b42a162a92be3e7d2d88c2bdd54ced5

20
install.py Normal file
View File

@@ -0,0 +1,20 @@
import launch
import os
import pkg_resources
req_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt")
with open(req_file) as file:
for package in file:
try:
package = package.strip()
if '==' in package:
package_name, package_version = package.split('==')
installed_version = pkg_resources.get_distribution(package_name).version
if installed_version != package_version:
launch.run_pip(f"install {package}", f"sd-webui-prompt-all-in-one requirement: changing {package_name} version from {installed_version} to {package_version}")
elif not launch.is_installed(package):
launch.run_pip(f"install {package}", f"sd-webui-prompt-all-in-one: {package}")
except Exception as e:
print(e)
print(f'Warning: Failed to install {package}, some preprocessors may not work.')

View File

@@ -0,0 +1 @@
531a9e18d7dd3f66bb2ab5357b2236c577ae2914

View File

@@ -0,0 +1 @@
417d2bb2b3a4001531f19131eed4986f9c14ce88

8
requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
translators
fastapi
hashlib
openai
boto3
aliyun-python-sdk-core
aliyun-python-sdk-alimt
tencentcloud-sdk-python

Binary file not shown.

Binary file not shown.

Binary file not shown.

11
scripts/get_extensions.py Normal file
View File

@@ -0,0 +1,11 @@
import os
from pathlib import Path
def get_extensions():
extends_dir = os.path.join(Path().absolute(), 'extensions')
extends = []
for name in os.listdir(extends_dir):
path = os.path.join(extends_dir, name)
if os.path.isdir(path):
extends.append(name)
return extends

14
scripts/get_i18n.py Normal file
View File

@@ -0,0 +1,14 @@
import os
import json
i18n = {}
def get_i18n(reload=False):
global i18n
if reload or not i18n:
i18n = {}
current_dir = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(current_dir, '../i18n.json')
config_file = os.path.normpath(config_file)
with open(config_file, 'r', encoding='utf8') as f:
i18n = json.load(f)
return i18n

View File

@@ -0,0 +1,21 @@
from modules import script_callbacks, extra_networks, prompt_parser
from modules.sd_hijack import model_hijack
from functools import partial, reduce
def get_token_counter(text, steps):
# copy from modules.ui.py
try:
text, _ = extra_networks.parse_prompt(text)
_, prompt_flat_list, _ = prompt_parser.get_multicond_prompt_list([text])
prompt_schedules = prompt_parser.get_learned_conditioning_prompt_schedules(prompt_flat_list, steps)
except Exception:
# a parsing error can happen here during typing, and we don't want to bother the user with
# messages related to it in console
prompt_schedules = [[[steps, text]]]
flat_prompts = reduce(lambda list1, list2: list1+list2, prompt_schedules)
prompts = [prompt_text for step, prompt_text in flat_prompts]
token_count, max_length = max([model_hijack.get_prompt_lengths(prompt) for prompt in prompts], key=lambda args: args[0])
return {"token_count": token_count, "max_length": max_length}

View File

@@ -0,0 +1,35 @@
import os
import json
# from scripts.storage import storage
translate_apis = {}
# st = storage()
def get_translate_apis(reload=False):
global translate_apis
global st
if reload or not translate_apis:
translate_apis = {}
current_dir = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(current_dir, '../translate_apis.json')
config_file = os.path.normpath(config_file)
with open(config_file, 'r', encoding='utf8') as f:
translate_apis = json.load(f)
# for group in translate_apis['apis']:
# for item in group['children']:
# if 'config' not in item:
# continue
# config_name = 'translate_api.' + item['key']
# config = st.get(config_name)
# if not config:
# config = {}
# for config_item in item['config']:
# if config_item['key'] in config:
# config_item['value'] = config[config_item['key']]
# else:
# if 'default' in config_item:
# config_item['value'] = config_item['default']
# else:
# config_item['value'] = ''
return translate_apis

152
scripts/history.py Normal file
View File

@@ -0,0 +1,152 @@
from scripts.storage import storage
import uuid
import time
class history:
histoies = {
'txt2img': [],
'txt2img_neg': [],
'img2img': [],
'img2img_neg': [],
}
favorites = {
'txt2img': [],
'txt2img_neg': [],
'img2img': [],
'img2img_neg': [],
}
max = 100
storage = storage()
def __init__(self):
for type in self.histoies:
self.histoies[type] = self.storage.get('history.' + type)
if self.histoies[type] is None:
self.histoies[type] = []
self.__save_histories(type)
for type in self.favorites:
self.favorites[type] = self.storage.get('favorite.' + type)
if self.favorites[type] is None:
self.favorites[type] = []
self.__save_favorites(type)
def __save_histories(self, type):
self.storage.set('history.' + type, self.histoies[type])
def __save_favorites(self, type):
self.storage.set('favorite.' + type, self.favorites[type])
def get_histoies(self, type):
histoies = self.histoies[type]
for history in histoies:
history['is_favorite'] = self.is_favorite(type, history['id'])
return histoies
def is_favorite(self, type, id):
for favorite in self.favorites[type]:
if favorite['id'] == id:
return True
return False
def get_favorites(self, type):
return self.favorites[type]
def push_history(self, type, tags, prompt, name=''):
if len(self.histoies[type]) >= self.max:
self.histoies[type].pop()
item = {
'id': str(uuid.uuid1()),
'time': int(time.time()),
'name': name,
'tags': tags,
'prompt': prompt,
}
self.histoies[type].append(item)
self.__save_histories(type)
return item
def get_latest_history(self, type):
if len(self.histoies[type]) > 0:
return self.histoies[type][-1]
return None
def set_history(self, type, id, tags, prompt, name):
for history in self.histoies[type]:
if history['id'] == id:
history['tags'] = tags
history['prompt'] = prompt
history['name'] = name
self.__save_histories(type)
if self.is_favorite(type, id):
self.set_favorite(type, id, tags, prompt, name)
return True
return False
def set_favorite(self, type, id, tags, prompt, name):
for favorite in self.favorites[type]:
if favorite['id'] == id:
favorite['tags'] = tags
favorite['prompt'] = prompt
favorite['name'] = name
self.__save_favorites(type)
return True
return False
def set_history_name(self, type, id, name):
for history in self.histoies[type]:
if history['id'] == id:
history['name'] = name
self.__save_histories(type)
for favorite in self.favorites[type]:
if favorite['id'] == id:
favorite['name'] = name
self.__save_favorites(type)
return True
return False
def set_favorite_name(self, type, id, name):
for favorite in self.favorites[type]:
if favorite['id'] == id:
favorite['name'] = name
self.__save_favorites(type)
for history in self.histoies[type]:
if history['id'] == id:
history['name'] = name
self.__save_histories(type)
return True
return False
def dofavorite(self, type, id):
if self.is_favorite(type, id):
return False
for history in self.histoies[type]:
if history['id'] == id:
self.favorites[type].append(history)
self.__save_favorites(type)
return True
return False
def unfavorite(self, type, id):
if not self.is_favorite(type, id):
return False
for favorite in self.favorites[type]:
if favorite['id'] == id:
self.favorites[type].remove(favorite)
self.__save_favorites(type)
return True
return False
def remove_history(self, type, id):
for history in self.histoies[type]:
if history['id'] == id:
self.histoies[type].remove(history)
self.__save_histories(type)
return True
return False
def remove_histories(self, type):
self.histoies[type] = []
self.__save_histories(type)
return True

186
scripts/on_app_started.py Executable file
View File

@@ -0,0 +1,186 @@
import gradio as gr
import os
from pathlib import Path
from modules import script_callbacks, extra_networks, prompt_parser
from fastapi import FastAPI, Body, Request
from scripts.storage import storage
from scripts.get_extensions import get_extensions
from scripts.get_token_counter import get_token_counter
from scripts.get_i18n import get_i18n
from scripts.get_translate_apis import get_translate_apis
from scripts.translate import translate
from scripts.history import history
VERSION = '0.0.1'
def on_app_started(_: gr.Blocks, app: FastAPI):
st = storage()
hi = history()
@app.get("/physton_prompt/get_version")
async def _get_version():
return {"version": VERSION}
@app.get("/physton_prompt/get_config")
async def _get_config():
return {
'i18n': get_i18n(True),
'translate_apis': get_translate_apis(True),
}
@app.get("/physton_prompt/get_extensions")
async def _get_extensions():
return {"extends": get_extensions()}
@app.post("/physton_prompt/token_counter")
async def _token_counter(request: Request):
data = await request.json()
if 'text' not in data or 'steps' not in data:
return {"success": False, "message": "text or steps is required"}
return get_token_counter(data['text'], data['steps'])
@app.get("/physton_prompt/get_data")
async def _get_data(key: str):
return {"data": st.get(key)}
@app.get("/physton_prompt/get_datas")
async def _get_datas(keys: str):
keys = keys.split(',')
datas = {}
for key in keys:
datas[key] = st.get(key)
return {"datas": datas}
@app.post("/physton_prompt/set_data")
async def _set_data(request: Request):
data = await request.json()
if 'key' not in data or 'data' not in data:
return {"success": False, "message": "key or data is required"}
st.set(data['key'], data['data'])
return {"success": True}
@app.post("/physton_prompt/set_datas")
async def _set_datas(request: Request):
data = await request.json()
if not isinstance(data, dict):
return {"success": False, "message": "data is not dict"}
for key in data:
st.set(key, data[key])
return {"success": True}
@app.get("/physton_prompt/get_data_list_item")
async def _get_data_list_item(key: str, index: int):
return {"item": st.list_get(key, index)}
@app.post("/physton_prompt/push_data_list")
async def _push_data_list(request: Request):
data = await request.json()
if 'key' not in data or 'item' not in data:
return {"success": False, "message": "key or item is required"}
st.list_push(data['key'], data['item'])
return {"success": True}
@app.post("/physton_prompt/pop_data_list")
async def _pop_data_list(request: Request):
data = await request.json()
if 'key' not in data:
return {"success": False, "message": "key is required"}
return {"success": True, 'item': st.list_pop(data['key'])}
@app.post("/physton_prompt/shift_data_list")
async def _shift_data_list(request: Request):
data = await request.json()
if 'key' not in data:
return {"success": False, "message": "key is required"}
return {"success": True, 'item': st.list_shift(data['key'])}
@app.post("/physton_prompt/remove_data_list")
async def _remove_data_list(request: Request):
data = await request.json()
if 'key' not in data or 'index' not in data:
return {"success": False, "message": "key or index is required"}
st.list_remove(data['key'], data['index'])
return {"success": True}
@app.post("/physton_prompt/clear_data_list")
async def _clear_data_list(request: Request):
data = await request.json()
if 'key' not in data:
return {"success": False, "message": "key is required"}
st.list_clear(data['key'])
return {"success": True}
@app.get("/physton_prompt/get_histories")
async def _get_histories(type: str):
return {"histories": hi.get_histoies(type)}
@app.get("/physton_prompt/get_favorites")
async def _get_favorites(type: str):
return {"favorites": hi.get_favorites(type)}
@app.post("/physton_prompt/push_history")
async def _push_history(request: Request):
data = await request.json()
if 'type' not in data or 'tags' not in data or 'prompt' not in data:
return {"success": False, "message": "type or tags or prompt is required"}
hi.push_history(data['type'], data['tags'], data['prompt'], data.get('name', ''))
return {"success": True}
@app.get("/physton_prompt/get_latest_history")
async def _get_latest_history(type: str):
return {"history": hi.get_latest_history(type)}
@app.post("/physton_prompt/set_history")
async def _set_history(request: Request):
data = await request.json()
if 'type' not in data or 'id' not in data or 'tags' not in data or 'prompt' not in data or 'name' not in data:
return {"success": False, "message": "type or id or tags or prompt is required"}
return {"success": hi.set_history(data['type'], data['id'], data['tags'], data['prompt'], data['name'])}
@app.post("/physton_prompt/set_history_name")
async def _set_history_name(request: Request):
data = await request.json()
if 'type' not in data or 'id' not in data or 'name' not in data:
return {"success": False, "message": "type or id or name is required"}
return {"success": hi.set_history_name(data['type'], data['id'], data['name'])}
@app.post("/physton_prompt/set_favorite_name")
async def _set_favorite_name(request: Request):
data = await request.json()
if 'type' not in data or 'id' not in data or 'name' not in data:
return {"success": False, "message": "type or id or name is required"}
return {"success": hi.set_favorite_name(data['type'], data['id'], data['name'])}
@app.post("/physton_prompt/dofavorite")
async def _dofavorite(request: Request):
data = await request.json()
if 'type' not in data or 'id' not in data:
return {"success": False, "message": "type or id is required"}
return {"success": hi.dofavorite(data['type'], data['id'])}
@app.post("/physton_prompt/unfavorite")
async def _unfavorite(request: Request):
data = await request.json()
if 'type' not in data or 'id' not in data:
return {"success": False, "message": "type or id is required"}
return {"success": hi.unfavorite(data['type'], data['id'])}
@app.post("/physton_prompt/delete_history")
async def _delete_history(request: Request):
data = await request.json()
if 'type' not in data or 'id' not in data:
return {"success": False, "message": "type or id is required"}
return {"success": hi.remove_history(data['type'], data['id'])}
@app.post("/physton_prompt/delete_histories")
async def _delete_histories(request: Request):
data = await request.json()
if 'type' not in data:
return {"success": False, "message": "type is required"}
return {"success": hi.remove_histories(data['type'])}
@app.post("/physton_prompt/translate")
async def _translate(text: str = Body(...), from_lang: str = Body(...), to_lang: str = Body(...), api: str = Body(...), api_config: dict = Body(...)):
return translate(text, from_lang, to_lang, api, api_config)
script_callbacks.on_app_started(on_app_started)

144
scripts/storage.py Normal file
View File

@@ -0,0 +1,144 @@
import os
from pathlib import Path
import json
import time
class storage:
storage_path = ''
def __init__(self):
pass
def __get_storage_path(self):
self.get_storage_path = os.path.join(Path().absolute(), 'physton-prompt')
if not os.path.exists(self.get_storage_path):
os.makedirs(self.get_storage_path)
return self.get_storage_path
def __get_data_filename(self, key):
return self.__get_storage_path() + '/' + key + '.json'
def __get_key_lock_filename(self, key):
return self.__get_storage_path() + '/' + key + '.lock'
def __lock(self, key):
file_path = self.__get_key_lock_filename(key)
with open(file_path, 'w') as f:
f.write('1')
def __unlock(self, key):
file_path = self.__get_key_lock_filename(key)
if os.path.exists(file_path):
os.remove(file_path)
def __is_locked(self, key):
file_path = self.__get_key_lock_filename(key)
return os.path.exists(file_path)
def __get(self, key):
filename = self.__get_data_filename(key)
if not os.path.exists(filename):
return None
with open(filename, 'r') as f:
data = json.load(f)
return data
def __set(self, key, data):
file_path = self.__get_data_filename(key)
with open(file_path, 'w') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def set(self, key, data):
while self.__is_locked(key):
time.sleep(0.01)
self.__lock(key)
try:
self.__set(key, data)
self.__unlock(key)
except Exception as e:
self.__unlock(key)
raise e
def get(self, key):
return self.__get(key)
def delete(self, key):
file_path = self.__get_data_filename(key)
if os.path.exists(file_path):
os.remove(file_path)
def __get_list(self, key):
data = self.get(key)
if not data:
data = []
return data
# 向列表中添加元素
def list_push(self, key, item):
while self.__is_locked(key):
time.sleep(0.01)
self.__lock(key)
try:
data = self.__get_list(key)
data.append(item)
self.__set(key, data)
self.__unlock(key)
except Exception as e:
self.__unlock(key)
raise e
# 从列表中删除和返回最后一个元素
def list_pop(self, key):
while self.__is_locked(key):
time.sleep(0.01)
self.__lock(key)
try:
data = self.__get_list(key)
item = data.pop()
self.__set(key, data)
self.__unlock(key)
return item
except Exception as e:
self.__unlock(key)
raise e
# 从列表中删除和返回第一个元素
def list_shift(self, key):
while self.__is_locked(key):
time.sleep(0.01)
self.__lock(key)
try:
data = self.__get_list(key)
item = data.pop(0)
self.__set(key, data)
self.__unlock(key)
return item
except Exception as e:
self.__unlock(key)
raise e
# 从列表中删除指定元素
def list_remove(self, key, index):
while self.__is_locked(key):
time.sleep(0.01)
self.__lock(key)
data = self.__get_list(key)
data.pop(index)
self.__set(key, data)
self.__unlock(key)
# 获取列表中指定位置的元素
def list_get(self, key, index):
data = self.__get_list(key)
return data[index]
# 清空列表中的所有元素
def list_clear(self, key):
while self.__is_locked(key):
time.sleep(0.01)
self.__lock(key)
try:
self.__set(key, [])
self.__unlock(key)
except Exception as e:
self.__unlock(key)
raise e

321
scripts/translate.py Normal file
View File

@@ -0,0 +1,321 @@
from scripts.get_translate_apis import get_translate_apis
import hashlib
import os
import requests
import uuid
import random
import json
import time
caches = {}
def translate_google(text, from_lang, to_lang, api_config):
url = 'https://translation.googleapis.com/language/translate/v2/'
api_key = api_config.get('api_key', '')
if not api_key:
raise Exception("api_key is required")
params = {
'key': api_key,
'q': text,
'source': from_lang,
'target': to_lang,
'format': 'text'
}
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'error' in result:
raise Exception(result['error']['message'])
return result['data']['translations'][0]['translatedText']
def translate_openai(text, from_lang, to_lang, api_config):
import openai
openai.api_key = api_config.get('api_key', '')
model = api_config.get('model', 'gpt-3.5-turbo')
if not openai.api_key:
raise Exception("api_key is required")
messages = [
{"role": "system", "content": "You are a translator assistant."},
{"role":
"user",
"content": f"You are a translator assistant. Please translate the following JSON data {to_lang}. Preserve the original format. Only return the translation result, without any additional content or annotations. If the prompt word is in the target language, please send it to me unchanged:\n{text}"
},
]
completion = openai.ChatCompletion.create(model=model, messages=messages, timeout=10)
if len(completion.choices) == 0:
raise Exception("No response from OpenAI")
content = completion.choices[0].message.content
return content
def translate_microsoft(text, from_lang, to_lang, api_config):
url = 'https://api.cognitive.microsofttranslator.com/translate'
api_key = api_config.get('api_key', '')
region = api_config.get('region', '')
if not api_key:
raise Exception("api_key is required")
if not region:
raise Exception("region is required")
params = {
'api-version': '3.0',
'from': from_lang,
'to': to_lang
}
headers = {
'Ocp-Apim-Subscription-Key': api_key,
'Ocp-Apim-Subscription-Region': region,
'Content-type': 'application/json',
'X-ClientTraceId': str(uuid.uuid4())
}
body = [{
'text': text
}]
response = requests.post(url, params=params, headers=headers, json=body, timeout=10)
result = response.json()
if 'error' in result:
raise Exception(result['error']['message'])
if len(result) == 0:
raise Exception("No response from Microsoft")
return result[0]['translations'][0]['text']
def translate_amazon(text, from_lang, to_lang, api_config):
import boto3
api_key_id = api_config.get('api_key_id', '')
api_key_secret = api_config.get('api_key_secret', '')
region = api_config.get('region', '')
if not api_key_id:
raise Exception("api_key_id is required")
if not api_key_secret:
raise Exception("api_key_secret is required")
if not region:
raise Exception("region is required")
translate = boto3.client(service_name='translate', region_name=region, use_ssl=True, aws_access_key_id=api_key_id, aws_secret_access_key=api_key_secret)
result = translate.translate_text(Text=text, SourceLanguageCode=from_lang, TargetLanguageCode=to_lang)
if 'TranslatedText' not in result:
raise Exception("No response from Amazon")
return result['TranslatedText']
def translate_deepl(text, from_lang, to_lang, api_config):
url = 'https://api-free.deepl.com/v2/translate'
api_key = api_config.get('api_key', '')
if not api_key:
raise Exception("api_key is required")
headers = {"Authorization": f"DeepL-Auth-Key {api_key}"}
data = {
'text': text,
'source_lang': from_lang,
'target_lang': to_lang
}
response = requests.post(url, headers=headers, data=data, timeout=10)
result = response.json()
if 'message' in result:
raise Exception(result['message'])
if 'translations' not in result:
raise Exception("No response from DeepL")
return result['translations'][0]['text']
def translate_baidu(text, from_lang, to_lang, api_config):
url = "https://fanyi-api.baidu.com/api/trans/vip/translate"
app_id = api_config.get('app_id', '')
app_secret = api_config.get('app_secret', '')
if not app_id:
raise Exception("app_id is required")
if not app_secret:
raise Exception("app_secret is required")
salt = random.randint(32768, 65536)
sign = app_id + text + str(salt) + app_secret
sign = hashlib.md5(sign.encode()).hexdigest()
params = {
'q': text,
'from': from_lang,
'to': to_lang,
'appid': app_id,
'salt': salt,
'sign': sign
}
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'error_code' in result:
raise Exception(result['error_msg'])
if 'trans_result' not in result:
raise Exception("No response from Baidu")
translated_text = ''
for item in result['trans_result']:
translated_text += "\n" + item['dst']
return translated_text
# return result['trans_result'][0]['dst']
def translate_youdao(text, from_lang, to_lang, api_config):
url = "https://openapi.youdao.com/api"
app_id = api_config.get('app_id', '')
app_secret = api_config.get('app_secret', '')
if not app_id:
raise Exception("app_id is required")
if not app_secret:
raise Exception("app_secret is required")
curtime = str(int(time.time()))
salt = random.randint(32768, 65536)
if(len(text) <= 20):
input = text
elif(len(text) > 20):
input = text[:10] + str(len(text)) + text[-10:]
sign = app_id + input + str(salt) + curtime + app_secret
sign = hashlib.sha256(sign.encode()).hexdigest()
params = {
'q': text,
'from': from_lang,
'to': to_lang,
'appKey': app_id,
'salt': salt,
'signType': 'v3',
'curtime': curtime,
'sign': sign
}
headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
response = requests.post(url, params=params, timeout=10, headers=headers)
result = response.json()
if 'errorCode' not in result:
raise Exception("No response from Youdao")
if result['errorCode'] != '0':
raise Exception(f'errorCode: {result["errorCode"]}')
return result['translation'][0]
def translate_alibaba(text, from_lang, to_lang, api_config):
access_key_id = api_config.get('access_key_id', '')
access_key_secret = api_config.get('access_key_secret', '')
region = api_config.get('region', 'cn-shanghai')
if not access_key_id:
raise Exception("access_key_id is required")
if not access_key_secret:
raise Exception("access_key_secret is required")
if not region:
raise Exception("region is required")
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
from aliyunsdkalimt.request.v20181012 import TranslateRequest
client = AcsClient(access_key_id, access_key_secret, region)
request = TranslateRequest.TranslateRequest()
request.set_SourceLanguage(from_lang)
request.set_Scene("general")
request.set_SourceText(text)
request.set_FormatType("text") #翻译文本的格式
request.set_TargetLanguage(to_lang)
request.set_method("POST")
response = client.do_action_with_exception(request)
result = json.loads(response)
if 'Code' not in result:
raise Exception("No response from Alibaba")
if result['Code'] != '200':
raise Exception(result['Message'])
return result['Data']['Translated']
def translate_tencent(text, from_lang, to_lang, api_config):
secret_id = api_config.get('secret_id', '')
secret_key = api_config.get('secret_key', '')
region = api_config.get('region', 'ap-shanghai')
if not secret_id:
raise Exception("secret_id is required")
if not secret_key:
raise Exception("secret_key is required")
if not region:
raise Exception("region is required")
from tencentcloud.tmt.v20180321 import models
from tencentcloud.common import credential
from tencentcloud.tmt.v20180321 import tmt_client
request = models.TextTranslateRequest()
request.SourceText = text
request.Source = from_lang
request.Target = to_lang
request.ProjectId = 0
cred = credential.Credential(secret_id, secret_key)
client = tmt_client.TmtClient(cred, region)
response = client.TextTranslate(request)
result = json.loads(response.to_json_string())
if 'Error' in result:
raise Exception(result['Error']['Message'])
if 'TargetText' not in result:
raise Exception("No response from Tencent")
return result['TargetText']
def translate(text, from_lang, to_lang, api, api_config = {}):
global caches
if from_lang == 'zh_CN' or from_lang == 'zh_TW' or to_lang == 'zh_CN' or to_lang == 'zh_TW':
os.environ['translators_default_region'] = 'China'
else:
os.environ['translators_default_region'] = 'EN'
result = {
"success": False,
"message": "",
"text": text,
"translated_text": "",
"from_lang": from_lang,
"to_lang": to_lang,
"api": api,
"api_config": api_config
}
try:
apis = get_translate_apis()
find = False
for group in apis['apis']:
for item in group['children']:
if item['key'] == api:
find = item
break
if not find:
result['message'] = 'translate_api_not_found'
return result
# 检查语言是否支持
from_lang = find['support'].get(from_lang, False)
to_lang = find['support'].get(to_lang, False)
if not from_lang or not to_lang:
result['message'] = 'translate_language_not_support'
return result
cache_name = f'{api}.{from_lang}.{to_lang}.{text}.' + json.dumps(api_config)
cache_name = hashlib.md5(cache_name.encode('utf-8')).hexdigest()
if cache_name in caches:
result['translated_text'] = caches[cache_name]
result['success'] = True
return result
# print(find)
if find['key'] == 'google':
result['translated_text'] = translate_google(text, from_lang, to_lang, api_config)
elif find['key'] == 'openai':
result['translated_text'] = translate_openai(text, from_lang, to_lang, api_config)
elif find['key'] == 'microsoft':
result['translated_text'] = translate_microsoft(text, from_lang, to_lang, api_config)
elif find['key'] == 'amazon':
result['translated_text'] = translate_amazon(text, from_lang, to_lang, api_config)
elif find['key'] == 'deepl':
result['translated_text'] = translate_deepl(text, from_lang, to_lang, api_config)
elif find['key'] == 'baidu':
result['translated_text'] = translate_baidu(text, from_lang, to_lang, api_config)
elif find['key'] == 'alibaba':
result['translated_text'] = translate_alibaba(text, from_lang, to_lang, api_config)
elif find['key'] == 'youdao':
result['translated_text'] = translate_youdao(text, from_lang, to_lang, api_config)
elif find['key'] == 'tencent':
result['translated_text'] = translate_tencent(text, from_lang, to_lang, api_config)
elif 'type' in find and find['type'] == 'translators':
import translators as ts
result['translated_text'] = ts.translate_text(text, from_language=from_lang, to_language=to_lang, translator=find['translator'], timeout=10)
else:
result['message'] = 'translate_api_not_support'
return result
caches[cache_name] = result['translated_text']
result['success'] = True
return result
except Exception as e:
print(e)
result['message'] = str(e)
return result

28
src/.gitignore vendored Executable file
View File

@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

29
src/README.md Executable file
View File

@@ -0,0 +1,29 @@
# sd-webui-prompt-all-in-one
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

13
src/index.html Executable file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

25
src/package.json Executable file
View File

@@ -0,0 +1,25 @@
{
"name": "sd-webui-prompt-all-in-one",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"autosize-input": "^1.0.2",
"axios": "^1.4.0",
"sortablejs": "^1.15.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"vite": "^4.1.4"
}
}

314
src/src/App.vue Executable file
View File

@@ -0,0 +1,314 @@
<template>
<div>
<block v-for="(item) in prompts" :key="item.name">
<physton-prompt v-if="item.$textarea" :id="item.id" :ref="item.id" :name="item.name"
:neg="item.neg" :textarea="item.$textarea" :steps="item.$steps"
v-model:language-code="languageCode"
:translate-apis="translateApis" :languages="languages"
:history-key="item.historyKey" :favorite-key="item.favoriteKey"
v-model:auto-translate-to-english="autoTranslateToEnglish"
v-model:auto-translate-to-local="autoTranslateToLocal"
v-model:hide-default-input="hideDefaultInput"
v-model:enable-tooltip="enableTooltip"
v-model:translate-api="translateApi"
:translate-api-config="translateApiConfig"
@click:translate-api="onTranslateApiClick"></physton-prompt>
</block>
<translate-setting ref="translateSetting" v-model:language-code="languageCode"
:translate-apis="translateApis" :languages="languages"
v-model:translate-api="translateApi"></translate-setting>
</div>
</template>
<script>
import PhystonPrompt from "./components/phystonPrompt.vue"
import TranslateSetting from "@/components/translateSetting.vue";
import common from "@/utils/common";
export default {
name: 'App',
components: {
TranslateSetting,
PhystonPrompt,
},
mixins: [],
data() {
return {
prompts: [
{
prompt: 'txt2img_prompt',
counter: 'txt2img_token_counter',
button: 'txt2img_token_button',
steps: 'txt2img_steps',
historyKey: 'txt2img',
history: [],
favoriteKey: 'txt2img',
favorite: [],
$prompt: null,
$textarea: null,
$steps: null,
name: 'txt2img_prompt',
neg: false,
id: 'phystonPrompt_txt2img_prompt'
},
{
prompt: 'txt2img_neg_prompt',
counter: 'txt2img_negative_token_counter',
button: 'txt2img_negative_token_button',
steps: 'txt2img_steps',
historyKey: 'txt2img_neg',
history: [],
favoriteKey: 'txt2img_neg',
favorite: [],
$prompt: null,
$textarea: null,
$steps: null,
name: 'txt2img_neg_prompt',
neg: true,
id: 'phystonPrompt_txt2img_neg_prompt'
},
{
prompt: 'img2img_prompt',
counter: 'img2img_token_counter',
button: 'img2img_token_button',
steps: 'img2img_steps',
historyKey: 'img2img',
history: [],
favoriteKey: 'img2img',
favorite: [],
$prompt: null,
$textarea: null,
$steps: null,
name: 'img2img_prompt',
neg: false,
id: 'phystonPrompt_img2img_prompt'
},
{
prompt: 'img2img_neg_prompt',
counter: 'img2img_negative_token_counter',
button: 'img2img_negative_token_button',
steps: 'img2img_steps',
historyKey: 'img2img_neg',
history: [],
favoriteKey: 'img2img_neg',
favorite: [],
$prompt: null,
$textarea: null,
$steps: null,
name: 'img2img_neg_prompt',
neg: true,
id: 'phystonPrompt_img2img_neg_prompt'
},
],
languageCode: '',
languages: {},
translateApis: [],
translateApi: '',
translateApiConfig: {},
autoTranslateToEnglish: false,
autoTranslateToLocal: false,
hideDefaultInput: false,
enableTooltip: true,
startWatchSave: false,
}
},
watch: {
languageCode: {
handler: function (val, oldVal) {
if (!this.startWatchSave) return
console.log('onLanguageCodeChange', val)
this.gradioAPI.setData('languageCode', val).then(data => {
}).catch(err => {
})
},
immediate: false,
},
autoTranslateToEnglish: {
handler: function (val, oldVal) {
if (!this.startWatchSave) return
console.log('onAutoTranslateToEnglishChange', val)
this.gradioAPI.setData('autoTranslateToEnglish', val).then(data => {
}).catch(err => {
})
},
immediate: false,
},
autoTranslateToLocal: {
handler: function (val, oldVal) {
if (!this.startWatchSave) return
console.log('onAutoTranslateToLocalChange', val)
this.gradioAPI.setData('autoTranslateToLocal', val).then(data => {
}).catch(err => {
})
},
immediate: false,
},
hideDefaultInput: {
handler: function (val, oldVal) {
if (!this.startWatchSave) return
console.log('onHideDefaultInputChange', val)
this.prompts.forEach(item => {
item.$prompt.parentElement.parentElement.style.display = val ? 'none' : 'flex'
})
this.gradioAPI.setData('hideDefaultInput', val).then(data => {
}).catch(err => {
})
},
immediate: false,
},
enableTooltip: {
handler: function (val, oldVal) {
if (!this.startWatchSave) return
localStorage.setItem('phystonPromptEnableTooltip', val ? 'true' : 'false')
this.updateTippyState()
this.gradioAPI.setData('enableTooltip', val).then(data => {
}).catch(err => {
})
},
immediate: false,
},
translateApi: {
handler: function (val, oldVal) {
if (!this.startWatchSave) return
console.log('onTranslateApiChange', val, oldVal)
this.updateTranslateApiConfig()
this.gradioAPI.setData('translateApi', val).then(data => {
}).catch(err => {
})
},
immediate: false,
},
},
mounted() {
this.gradioAPI.getConfig().then(res => {
console.log('config:', res)
this.languageCode = res.i18n.default
this.translateApi = res.translate_apis.default
this.translateApis = res.translate_apis.apis
let languages = {}
res.i18n.languages.forEach(lang => {
languages[lang.code] = lang
})
this.languages = languages
this.init()
}).catch(err => {
this.$toastr.error('Failed to connect to Gradio API: ' + err)
console.log(err)
})
},
methods: {
init() {
let dataListsKeys = ['languageCode', 'autoTranslateToEnglish', 'autoTranslateToLocal', 'hideDefaultInput', 'translateApi', 'enableTooltip']
/*this.prompts.forEach(item => {
dataListsKeys.push(item.historyKey)
dataListsKeys.push(item.favoriteKey)
})*/
this.gradioAPI.getDatas(dataListsKeys).then(data => {
if (data.languageCode !== null) {
let findLang = false
for (let key in this.languages) {
if (this.languages[key].code === data.languageCode) {
findLang = true
break
}
}
if (findLang) {
this.languageCode = data.languageCode
this.$forceUpdate()
}
}
if (data.autoTranslateToEnglish !== null) {
this.autoTranslateToEnglish = data.autoTranslateToEnglish
}
if (data.autoTranslateToLocal !== null) {
this.autoTranslateToLocal = data.autoTranslateToLocal
}
if (data.hideDefaultInput !== null) {
this.hideDefaultInput = data.hideDefaultInput
}
if (data.enableTooltip !== null) {
this.enableTooltip = data.enableTooltip
localStorage.setItem('phystonPromptEnableTooltip', this.enableTooltip ? 'true' : 'false')
this.updateTippyState()
}
if (data.translateApi !== null) {
this.translateApi = data.translateApi
}
this.updateTranslateApiConfig()
this.prompts.forEach(item => {
item.$prompt = document.getElementById(item.prompt)
item.$textarea = item.$prompt.getElementsByTagName("textarea")[0]
item.$steps = document.getElementById(item.steps)
// item.history = data[item.historyKey] || []
// item.favorite = data[item.favoriteKey] || []
})
this.$nextTick(() => {
this.prompts.forEach(item => {
const $prompt = document.getElementById(item.id)
item.$prompt.parentElement.parentElement.parentElement.appendChild($prompt)
item.$prompt.parentElement.parentElement.style.display = data.hideDefaultInput ? 'none' : 'flex'
// item.$textarea.parentNode.appendChild($prompt)
})
this.startWatchSave = true
})
// this.$refs.translateSetting.open(this.translateApi)
})
},
updateTippyState() {
for (const $tippy of this.$tippyList) {
if (this.enableTooltip) {
$tippy.enable()
} else {
$tippy.disable()
}
}
},
updateTranslateApiConfig() {
this.gradioAPI.getData('translate_api.' + this.translateApi).then(res => {
let config = {}
const apiItem = common.getTranslateApiItem(this.translateApis, this.translateApi)
if (apiItem) {
for (const item of apiItem.config) {
if (res) {
config[item.key] = res[item.key]
} else {
config[item.key] = item.default || ''
}
}
config['concurrent'] = apiItem.concurrent || 0
}
this.translateApiConfig = config
})
},
onTranslateApiClick() {
this.$refs.translateSetting.open(this.translateApi)
}
},
}
</script>
<style lang="less">
@import "toastr/build/toastr.min.css";
@import "tippy.js/dist/tippy.css";
.hover-scale-120 {
animation: all 0.3s;
&:hover {
transform: scale(1.2);
}
}
.hover-scale-140 {
animation: all 0.3s;
&:hover {
transform: scale(1.4);
}
}
</style>

View File

@@ -0,0 +1,410 @@
<template>
<div class="physton-prompt-favorite" ref="favorite" :style="style" @mouseenter="onMouseEnter"
@mouseleave="onMouseLeave">
<div class="favorite-content">
<div class="favorite-detail" v-show="currentItem && currentItem.tags">
<div class="favorite-item-tags">
<div class="favorite-item-tag" v-for="(tag, index) in currentItem.tags" :key="index">
<div class="item-tag-value">{{ tag.value }}</div>
<div class="item-tag-local-value">{{ tag.localValue }}</div>
</div>
</div>
</div>
<div class="favorite-list" v-show="favorites.length > 0" :style="{height: defaultHeight + 'px'}">
<div class="favorite-item" v-for="(item, index) in favorites" :key="item.id"
@mouseenter="onItemMouseEnter(index)" @mouseleave="onItemMouseLeave(index)">
<div class="favorite-item-header">
<div class="item-header-left">
<div class="item-header-index">{{ favorites.length - index }}</div>
<div class="item-header-time">{{ formatTime(item.time) }}</div>
<div class="item-header-name">
<input class="header-name-input" :value="item.name"
@keydown="onNameKeyDown(index, $event)"
@change="onNameChange(index, $event)" :placeholder="getLang('unset_name')">
</div>
</div>
<div class="item-header-right">
<div class="header-btn-favorite hover-scale-140" @click="onFavoriteClick(index)"
v-show="item.is_favorite" v-tooltip="getLang('remove_from_favorite')">
<icon-favorite-state :is-favorite="true" width="20" height="20"/>
</div>
<div class="header-btn-favorite hover-scale-140" @click="onFavoriteClick(index)"
v-show="!item.is_favorite" v-tooltip="getLang('add_to_favorite')">
<icon-favorite-state :is-favorite="false" width="20" height="20"/>
</div>
<div class="header-btn-copy hover-scale-140" @click="onCopyClick(index)"
v-tooltip="getLang('copy_to_clipboard')">
<icon-copy width="20" height="20" color="#fff"/>
</div>
<div class="header-btn-use hover-scale-140" @click="onUseClick(index)"
v-tooltip="getLang('use')">
<icon-use width="20" height="20" color="#fff"/>
</div>
</div>
</div>
<div class="favorite-item-prompt">{{ item.prompt }}</div>
</div>
</div>
</div>
<div class="favorite-empty" v-show="favorites.length === 0">
<icon-loading width="64" height="64" v-if="loading"/>
<span v-else>{{ emptyMsg }}</span>
</div>
</div>
</template>
<script>
import IconFavoriteState from "@/components/icons/iconFavoriteState.vue";
import common from "@/utils/common";
import IconLoading from "@/components/icons/iconLoading.vue";
import IconCopy from "@/components/icons/iconCopy.vue";
import LanguageMixin from "@/mixins/languageMixin";
import IconUse from "@/components/icons/iconUse.vue";
export default {
components: {IconUse, IconCopy, IconLoading, IconFavoriteState},
props: {
favoriteKey: {
type: String,
default: '',
required: true,
}
},
mixins: [LanguageMixin],
data() {
return {
favorites: [],
isShow: false,
top: 0,
left: 0,
loading: false,
emptyMsg: '',
defaultWidth: 500,
defaultHeight: 500,
style: {
top: 0,
left: 0,
width: 0,
height: 0,
overflow: 'hidden',
},
mouseEnter: false,
currentItem: {}
}
},
mounted() {
},
methods: {
formatTime(time) {
let now = new Date(time * 1000);
let year = now.getFullYear();
let month = now.getMonth() + 1;
if (month < 10) month = "0" + month;
let day = now.getDate();
if (day < 10) day = "0" + day;
let hour = now.getHours();
if (hour < 10) hour = "0" + hour;
let minute = now.getMinutes();
if (minute < 10) minute = "0" + minute;
let second = now.getSeconds();
if (second < 10) second = "0" + second;
return `${month}/${day} ${hour}:${minute}:${second}`
},
show($button) {
if (!$button) return
if (this.isShow) {
this._hide(0)
return
}
this.mouseEnter = false
this.favorites = []
let eWidth = $button.offsetWidth
let eHeight = $button.offsetHeight
let top = $button.offsetTop
let left = $button.offsetLeft + eWidth + 2
if (top + this.defaultHeight > window.innerHeight) top = window.innerHeight - this.defaultHeight
if (left + this.defaultWidth > window.innerWidth) left = window.innerWidth - this.defaultWidth
if (top < 0) top = 0
if (left < 0) left = 0
this.top = top
this.left = left
this._show()
this.gradioAPI.getFavorites(this.favoriteKey).then(res => {
// 倒序
res.reverse()
res.forEach(item => {
item.is_favorite = true
})
this.favorites = res
this.emptyMsg = this.getLang('no_favorite')
this.loading = false
}).catch(err => {
this.emptyMsg = this.getLang('get_favorite_error')
this.loading = false
})
// 如果n秒后鼠标还没进来就隐藏
setTimeout(() => {
if (this.mouseEnter) return
this._hide(0)
}, 3000)
},
_show() {
this.isShow = true
this.style.top = this.top + 'px'
this.style.left = this.left + 'px'
this.style.width = this.defaultWidth + 'px'
this.style.height = this.defaultHeight + 'px'
this.style.overflow = 'visible'
},
_hide(timeout = 1000) {
this.isShow = false
setTimeout(() => {
if (this.isShow) return
this.style.overflow = 'hidden'
this.style.width = 0
this.style.height = 0
setTimeout(() => {
if (this.isShow) return
this.style.top = '-9999px'
this.style.left = '-9999px'
}, 200)
}, timeout)
},
hide(timeout = 1000) {
this._hide(timeout)
},
onMouseEnter() {
this.mouseEnter = true
this._show()
},
onMouseLeave() {
this.mouseEnter = false
this._hide()
},
onFavoriteClick(index) {
let favorite = this.favorites[index]
if (!favorite.is_favorite) {
this.gradioAPI.doFavorite(this.favoriteKey, favorite.id).then(res => {
if (res) {
this.favorites[index].is_favorite = true
}
})
} else {
this.gradioAPI.unFavorite(this.favoriteKey, favorite.id).then(res => {
if (res) {
this.favorites[index].is_favorite = false
}
})
}
},
onCopyClick(index) {
this.$copyText(this.favorites[index].prompt).then(() => {
this.$toastr.success("success!")
}).catch(() => {
this.$toastr.error("error!")
})
},
onNameKeyDown(index, e) {
if (e.keyCode === 13) {
// 离开焦点
e.target.blur()
// this.favorites[index].name = e.target.value
}
},
onNameChange(index, e) {
const value = e.target.value
this.gradioAPI.setFavoriteName(this.favoriteKey, this.favorites[index].id, value).then(res => {
if (res) {
this.favorites[index].name = value
} else {
e.target.value = this.favorites[index].name
}
}).catch(err => {
e.target.value = this.favorites[index].name
})
},
onItemMouseEnter(index) {
this.currentItem = this.favorites[index]
},
onItemMouseLeave(index) {
this.currentItem = {}
},
onUseClick(index) {
this._hide(0)
this.$emit('use', this.favorites[index])
},
}
}
</script>
<style lang="less" scoped>
.physton-prompt-favorite {
position: absolute;
z-index: 999;
width: 0;
height: 0;
padding: 0;
box-shadow: 0 0 3px 0 #4a54ff;
border-radius: 6px 6px 4px 4px;
background-color: rgba(30, 30, 30, .9);
transition: height .1s ease-in-out, width .1s ease-in-out;
top: -9999px;
left: -9999px;
.favorite-content {
position: relative;
}
.favorite-detail {
position: absolute;
right: -404px;
top: 0;
width: 400px;
z-index: 1000;
background: center center #4A54FF;
background-image: linear-gradient(315deg, #6772FF 0, #00F9E5 100%);
background-size: 104% 104%;
box-shadow: 0 0 3px 0 #4a54ff;
border-radius: 6px 6px 4px 4px;
background-color: rgba(30, 30, 30, .9);
padding: 10px;
color: #1d1d1d;
.favorite-item-tags {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
.favorite-item-tag {
margin-right: 5px;
margin-bottom: 5px;
&:last-child {
margin-right: 0;
}
.item-tag-value {
padding: 4px 6px;
border-radius: 5px;
background: rgba(30, 30, 30, .9);
font-size: 12px;
color: #fff;
}
.item-tag-local-value {
font-size: 12px;
margin-top: 2px;
}
}
}
}
.favorite-list {
width: 100%;
height: 100%;
overflow: hidden;
overflow-y: auto;
.favorite-item {
padding: 6px 10px;
border-bottom: 1px solid #3c3c3c;
cursor: pointer;
&:hover {
background: center center #4A54FF;
background-image: linear-gradient(315deg, #6772FF 0, #00F9E5 100%);
background-size: 104% 104%;
.favorite-item-prompt {
overflow: visible;
white-space: normal;
color: #1d1d1d;
}
}
.favorite-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.item-header-left, .item-header-right {
display: flex;
justify-content: flex-start;
align-items: center;
> div {
margin-right: 10px;
font-size: 14px;
color: #fff;
&:last-child {
margin-right: 0;
}
}
}
.item-header-left {
.item-header-index {
background: #4A54FF;
padding: 2px 0;
width: 32px;
text-align: center;
}
.item-header-time {
width: 110px;
}
.item-header-name {
.header-name-input {
background: transparent;
border: 1px solid #3c3c3c;
height: 20px;
padding: 0 4px;
width: 210px;
font-size: 12px;
color: #00F9E5;
&:focus {
outline: none;
border-color: #4A54FF;
}
}
}
}
.item-header-right {
font-size: 12px;
color: #fff;
}
}
.favorite-item-prompt {
margin-top: 5px;
font-size: 14px;
line-height: 18px;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.favorite-empty {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 14px;
color: #999;
}
}
}
</style>

View File

@@ -0,0 +1,455 @@
<template>
<div class="physton-prompt-history" ref="history" :style="style" @mouseenter="onMouseEnter"
@mouseleave="onMouseLeave">
<div class="history-content">
<div class="history-detail" v-show="currentItem && currentItem.tags">
<div class="history-item-tags">
<div class="history-item-tag" v-for="(tag, index) in currentItem.tags" :key="index">
<div class="item-tag-value">{{ tag.value }}</div>
<div class="item-tag-local-value">{{ tag.localValue }}</div>
</div>
</div>
</div>
<div class="history-list" v-show="histories.length > 0" :style="{height: defaultHeight + 'px'}">
<div class="history-clear" @click="onDeleteAllHistoryClick">
<icon-remove width="18" height="18" color="#ff4a4a"></icon-remove>
{{ getLang('delete_all_history') }}
</div>
<div class="history-item" v-for="(item, index) in histories" :key="item.id"
@mouseenter="onItemMouseEnter(index)" @mouseleave="onItemMouseLeave(index)">
<div class="history-item-header">
<div class="item-header-left">
<div class="item-header-index">{{ histories.length - index }}</div>
<div class="item-header-time">{{ formatTime(item.time) }}</div>
<div class="item-header-name">
<input class="header-name-input" :value="item.name"
@keydown="onNameKeyDown(index, $event)"
@change="onNameChange(index, $event)" :placeholder="getLang('unset_name')">
</div>
</div>
<div class="item-header-right">
<div class="header-btn-favorite hover-scale-140" @click="onFavoriteClick(index)"
v-show="item.is_favorite" v-tooltip="getLang('remove_from_favorite')">
<icon-favorite-state :is-favorite="true" width="20" height="20"/>
</div>
<div class="header-btn-favorite hover-scale-140" @click="onFavoriteClick(index)"
v-show="!item.is_favorite" v-tooltip="getLang('add_to_favorite')">
<icon-favorite-state :is-favorite="false" width="20" height="20"/>
</div>
<div class="header-btn-copy hover-scale-140" @click="onCopyClick(index)"
v-tooltip="getLang('copy_to_clipboard')">
<icon-copy width="20" height="20" color="#fff"/>
</div>
<div class="header-btn-use hover-scale-140" @click="onUseClick(index)"
v-tooltip="getLang('use')">
<icon-use width="20" height="20" color="#fff"/>
</div>
</div>
</div>
<div class="history-item-prompt">{{ item.prompt }}</div>
</div>
</div>
</div>
<div class="history-empty" v-show="histories.length === 0">
<icon-loading width="64" height="64" v-if="loading"/>
<span v-else>{{ emptyMsg }}</span>
</div>
</div>
</template>
<script>
import IconFavoriteState from "@/components/icons/iconFavoriteState.vue";
import common from "@/utils/common";
import IconLoading from "@/components/icons/iconLoading.vue";
import IconCopy from "@/components/icons/iconCopy.vue";
import LanguageMixin from "@/mixins/languageMixin";
import IconUse from "@/components/icons/iconUse.vue";
import IconRemove from "@/components/icons/iconRemove.vue";
export default {
components: {IconRemove, IconUse, IconCopy, IconLoading, IconFavoriteState},
props: {
historyKey: {
type: String,
default: '',
required: true,
}
},
mixins: [LanguageMixin],
data() {
return {
histories: [],
isShow: false,
top: 0,
left: 0,
loading: false,
emptyMsg: '',
defaultWidth: 500,
defaultHeight: 600,
style: {
top: 0,
left: 0,
width: 0,
height: 0,
overflow: 'hidden',
},
mouseEnter: false,
currentItem: {}
}
},
mounted() {
},
methods: {
formatTime(time) {
let now = new Date(time * 1000);
let year = now.getFullYear();
let month = now.getMonth() + 1;
if (month < 10) month = "0" + month;
let day = now.getDate();
if (day < 10) day = "0" + day;
let hour = now.getHours();
if (hour < 10) hour = "0" + hour;
let minute = now.getMinutes();
if (minute < 10) minute = "0" + minute;
let second = now.getSeconds();
if (second < 10) second = "0" + second;
return `${month}/${day} ${hour}:${minute}:${second}`
},
show($button) {
if (!$button) return
if (this.isShow) {
this._hide(0)
return
}
this.mouseEnter = false
this.histories = []
let eWidth = $button.offsetWidth
let eHeight = $button.offsetHeight
let top = $button.offsetTop
let left = $button.offsetLeft + eWidth + 2
if (top + this.defaultHeight > window.innerHeight) top = window.innerHeight - this.defaultHeight
if (left + this.defaultWidth > window.innerWidth) left = window.innerWidth - this.defaultWidth
if (top < 0) top = 0
if (left < 0) left = 0
this.top = top
this.left = left
this._show()
this.gradioAPI.getHistories(this.historyKey).then(res => {
// 倒序
res.reverse()
this.histories = res
this.emptyMsg = this.getLang('no_history')
this.loading = false
}).catch(err => {
this.emptyMsg = this.getLang('get_history_error')
this.loading = false
})
// 如果n秒后鼠标还没进来就隐藏
setTimeout(() => {
if (this.mouseEnter) return
this._hide(0)
}, 3000)
},
_show() {
this.isShow = true
this.style.top = this.top + 'px'
this.style.left = this.left + 'px'
this.style.width = this.defaultWidth + 'px'
this.style.height = this.defaultHeight + 'px'
this.style.overflow = 'visible'
},
_hide(timeout = 1000) {
this.isShow = false
setTimeout(() => {
if (this.isShow) return
this.style.overflow = 'hidden'
this.style.width = 0
this.style.height = 0
setTimeout(() => {
if (this.isShow) return
this.style.top = '-9999px'
this.style.left = '-9999px'
}, 200)
}, timeout)
},
hide(timeout = 1000) {
this._hide(timeout)
},
onMouseEnter() {
this.mouseEnter = true
this._show()
},
onMouseLeave() {
this.mouseEnter = false
this._hide()
},
push(tags, prompt) {
if (!tags.length) return
this.gradioAPI.getLatestHistory(this.historyKey).then(res => {
if (res && res.prompt === prompt) {
// 如果有上一条记录并且prompt相同则更新
this.gradioAPI.setHistory(this.historyKey, res.id, tags, prompt, res.name).then(res => {
}).catch(err => {
})
} else {
this.gradioAPI.pushHistory(this.historyKey, tags, prompt).then(res => {
}).catch(err => {
})
}
}).catch(err => {
})
},
onFavoriteClick(index) {
let history = this.histories[index]
if (!history.is_favorite) {
this.gradioAPI.doFavorite(this.historyKey, history.id).then(res => {
if (res) {
this.histories[index].is_favorite = true
}
})
} else {
this.gradioAPI.unFavorite(this.historyKey, history.id).then(res => {
if (res) {
this.histories[index].is_favorite = false
}
})
}
},
onCopyClick(index) {
this.$copyText(this.histories[index].prompt).then(() => {
this.$toastr.success("success!")
}).catch(() => {
this.$toastr.error("error!")
})
},
onNameKeyDown(index, e) {
if (e.keyCode === 13) {
// 离开焦点
e.target.blur()
// this.histories[index].name = e.target.value
}
},
onNameChange(index, e) {
const value = e.target.value
this.gradioAPI.setFavoriteName(this.historyKey, this.histories[index].id, value).then(res => {
if (res) {
this.histories[index].name = value
} else {
e.target.value = this.histories[index].name
}
}).catch(err => {
e.target.value = this.histories[index].name
})
},
onItemMouseEnter(index) {
this.currentItem = this.histories[index]
},
onItemMouseLeave(index) {
this.currentItem = {}
},
onUseClick(index) {
this._hide(0)
this.$emit('use', this.histories[index])
},
onDeleteAllHistoryClick() {
if (!confirm(this.getLang('delete_all_history_confirm'))) return
this.gradioAPI.deleteHistories(this.historyKey).then(res => {
this.histories = []
}).catch(err => {
})
},
}
}
</script>
<style lang="less" scoped>
.physton-prompt-history {
position: absolute;
z-index: 999;
width: 0;
height: 0;
padding: 0;
box-shadow: 0 0 3px 0 #4a54ff;
border-radius: 6px 6px 4px 4px;
background-color: rgba(30, 30, 30, .9);
transition: height .1s ease-in-out, width .1s ease-in-out;
top: -9999px;
left: -9999px;
.history-content {
position: relative;
}
.history-detail {
position: absolute;
right: -404px;
top: 0;
width: 400px;
z-index: 1000;
background: center center #4A54FF;
background-image: linear-gradient(315deg, #6772FF 0, #00F9E5 100%);
background-size: 104% 104%;
box-shadow: 0 0 3px 0 #4a54ff;
border-radius: 6px 6px 4px 4px;
background-color: rgba(30, 30, 30, .9);
padding: 10px;
color: #1d1d1d;
.history-item-tags {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
.history-item-tag {
margin-right: 5px;
margin-bottom: 5px;
&:last-child {
margin-right: 0;
}
.item-tag-value {
padding: 4px 6px;
border-radius: 5px;
background: rgba(30, 30, 30, .9);
font-size: 12px;
color: #fff;
}
.item-tag-local-value {
font-size: 12px;
margin-top: 2px;
}
}
}
}
.history-list {
width: 100%;
height: 100%;
overflow: hidden;
overflow-y: auto;
position: relative;
.history-clear {
background: rgba(30, 30, 30, .9);
position: sticky;
top: 0;
padding: 10px 10px;
cursor: pointer;
border-bottom: 1px solid #4A54FF; // 6772FF
color: #ff4a4a;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
&:hover {
border-bottom: 1px solid #ff4a4a;
background: center center #4A54FF;
}
}
.history-item {
padding: 6px 10px;
border-bottom: 1px solid #3c3c3c;
cursor: pointer;
&:hover {
background: center center #4A54FF;
background-image: linear-gradient(315deg, #6772FF 0, #00F9E5 100%);
background-size: 104% 104%;
.history-item-prompt {
overflow: visible;
white-space: normal;
color: #1d1d1d;
}
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.item-header-left, .item-header-right {
display: flex;
justify-content: flex-start;
align-items: center;
> div {
margin-right: 10px;
font-size: 14px;
color: #fff;
&:last-child {
margin-right: 0;
}
}
}
.item-header-left {
.item-header-index {
background: #4A54FF;
padding: 2px 0;
width: 32px;
text-align: center;
}
.item-header-time {
width: 110px;
}
.item-header-name {
.header-name-input {
background: transparent;
border: 1px solid #3c3c3c;
height: 20px;
padding: 0 4px;
width: 210px;
font-size: 12px;
color: #00F9E5;
&:focus {
outline: none;
border-color: #4A54FF;
}
}
}
}
.item-header-right {
font-size: 12px;
color: #fff;
}
}
.history-item-prompt {
margin-top: 5px;
font-size: 14px;
line-height: 18px;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.history-empty {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
span {
font-size: 14px;
color: #999;
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 24 24" :style="{fill: color}">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconClose',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#000',
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683034246175" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3587" :width="width" :height="height" :style="{fill: color}">
<path d="M96.1 575.7a32.2 32.1 0 1 0 64.4 0 32.2 32.1 0 1 0-64.4 0Z" p-id="3588"></path>
<path d="M742.1 450.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.1 26-13.8 26-31s-11.7-31.3-26-31.4zM742.1 577.7l-269.5-2.1c-14.3-0.1-26 13.8-26 31s11.7 31.3 26 31.4l269.5 2.1c14.3 0.2 26-13.8 26-31s-11.7-31.3-26-31.4z" p-id="3589"></path>
<path d="M736.1 63.9H417c-70.4 0-128 57.6-128 128h-64.9c-70.4 0-128 57.6-128 128v128c-0.1 17.7 14.4 32 32.2 32 17.8 0 32.2-14.4 32.2-32.1V320c0-35.2 28.8-64 64-64H289v447.8c0 70.4 57.6 128 128 128h255.1c-0.1 35.2-28.8 63.8-64 63.8H224.5c-35.2 0-64-28.8-64-64V703.5c0-17.7-14.4-32.1-32.2-32.1-17.8 0-32.3 14.4-32.3 32.1v128.3c0 70.4 57.6 128 128 128h384.1c70.4 0 128-57.6 128-128h65c70.4 0 128-57.6 128-128V255.9l-193-192z m0.1 63.4l127.7 128.3H800c-35.2 0-64-28.8-64-64v-64.3h0.2z m64 641H416.1c-35.2 0-64-28.8-64-64v-513c0-35.2 28.8-64 64-64H671V191c0 70.4 57.6 128 128 128h65.2v385.3c0 35.2-28.8 64-64 64z" p-id="3590"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconCopy',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683017656179" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1536" :width="width" :height="height" :style="{fill: color}">
<path d="M512 32A480 480 0 1 0 992 512 480.64 480.64 0 0 0 512 32zM928 512a414.08 414.08 0 0 1-100.48 270.08L241.92 196.48A415.36 415.36 0 0 1 928 512z m-832 0a414.08 414.08 0 0 1 100.48-270.08l585.6 585.6A415.36 415.36 0 0 1 96 512z" p-id="1537"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconDisabled',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#d81e06',
}
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683017723924" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2069" id="mx_n_1683017723925" :width="width" :height="height" :style="{fill: color}">
<path d="M450.56 597.12L342.4 489.6l-44.8 44.8 147.84 148.48 342.4-273.92-39.68-49.92-297.6 238.08z" :fill="color" p-id="2070"></path>
<path d="M512 32A480 480 0 1 0 992 512 480.64 480.64 0 0 0 512 32z m0 896A416 416 0 1 1 928 512 416.64 416.64 0 0 1 512 928z" p-id="2071"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconEnable',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683125036613" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2659" :width="width" :height="height" :style="{fill: color}">
<path d="M229.248 704V337.504h271.744v61.984h-197.76v81.28h184v61.76h-184v99.712h204.768V704h-278.72z m550.496 0h-70.24v-135.488c0-28.672-1.504-47.232-4.48-55.648a39.04 39.04 0 0 0-14.656-19.616 41.792 41.792 0 0 0-24.384-7.008c-12.16 0-23.04 3.328-32.736 10.016-9.664 6.656-16.32 15.488-19.872 26.496-3.584 11.008-5.376 31.36-5.376 60.992V704h-70.24v-265.504h65.248v39.008c23.168-30.016 52.32-44.992 87.488-44.992 15.52 0 29.664 2.784 42.496 8.352 12.832 5.6 22.56 12.704 29.12 21.376 6.592 8.672 11.2 18.496 13.76 29.504 2.56 11.008 3.872 26.752 3.872 47.264V704z" p-id="2660"></path>
<path d="M160 144a32 32 0 0 0-32 32V864a32 32 0 0 0 32 32h688a32 32 0 0 0 32-32V176a32 32 0 0 0-32-32H160z m0-64h688a96 96 0 0 1 96 96V864a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V176a96 96 0 0 1 96-96z" p-id="2661"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconEnglish',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683129263830" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7484" :width="width" :height="height" :style="{fill: color}">
<path d="M814.2 1016.4c-19.6 0-39.2-4.5-57.1-13.5L530.8 889.2c-11.7-5.9-25.8-5.9-37.5 0L267 1002.9c-37.2 18.7-81.3 17.9-118.1-1.9-35.3-19-58.1-52.3-62.6-91.3-0.6-4.9-0.9-9.9-0.9-15V129.4C85.3 58 143.4 0 214.7 0h594.6c71.3 0 129.4 58 129.4 129.4v765.3c0 5.1-0.3 10.1-0.9 15-4.5 39-27.4 72.2-62.6 91.3-19 10.2-40 15.4-61 15.4zM512 799.5c19.6 0 39.2 4.5 57.1 13.5l226.3 113.7c12.3 6.2 27 5.9 39.3-0.8 6.1-3.3 16.7-11.1 18.4-26 0.2-1.7 0.3-3.4 0.3-5.2V129.4c0-24.3-19.8-44-44-44H214.7c-24.3 0-44 19.8-44 44v765.3c0 1.8 0.1 3.5 0.3 5.2 1.7 15 12.3 22.8 18.4 26 12.3 6.7 27 6.9 39.3 0.8L455 813c17.8-9.1 37.4-13.5 57-13.5z" p-id="7485"></path>
<path d="M597.8 590.6c-6.7 0-13.5-1.3-20-4.1L512 558.9l-65.7 27.7c-16.8 7-35.5 4.9-50.2-5.8-14.7-10.7-22.5-27.9-20.9-45.9l6-71-46.6-54c-11.9-13.7-15.6-32.2-10-49.4 5.6-17.3 19.5-30.1 37.1-34.2l69.5-16.2 36.9-61c9.4-15.5 25.9-24.8 44-24.8s34.6 9.3 44 24.8l36.9 61 69.5 16.2c17.7 4.1 31.6 16.9 37.2 34.2 5.6 17.2 1.8 35.8-10 49.5l-46.6 54 6 71c1.6 18-6.3 35.2-20.9 45.9-9.3 6.4-19.8 9.7-30.4 9.7z m-99-37.3s0.1 0 0 0z m26.4 0z m38.7-11.3c0 0.1 0 0.1 0 0z m-103.8 0z m51.9-71.3c6.8 0 13.7 1.4 20 4l27.1 11.4-2.5-29.4c-1.2-13.8 3.4-27.6 12.4-38.1l19.2-22.2-28.7-6.7c-13.4-3.2-25.1-11.7-32.2-23.3L512 341.1l-15.2 25.2c-7.2 11.8-18.9 20.3-32.1 23.4l-28.9 6.7 19.3 22.3c9 10.3 13.5 24.2 12.3 37.9l-2.5 29.4 27.2-11.4c6.3-2.6 13.1-3.9 19.9-3.9z m-121.6 3.9s0.1 0.1 0.1 0.2l-0.1-0.2z m243.3-0.2l-0.1 0.2s0-0.1 0.1-0.2z m-33.4-152.2h0.2-0.2z" p-id="7486"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconFavorite',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,39 @@
<template>
<div class="icon-svg icon-favorite" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683363779169" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7176"
data-spm-anchor-id="a313x.7781069.0.i11" :width="width" :height="height">
<path d="M447 134.6zM867.7 344.7l-187.3-27.2c-7.8-1.1-14.6-6-18.1-13.1l-83.7-169.7c-12.5-25.3-37.7-40.9-65.8-40.9-28.2 0-53.4 15.7-65.8 40.9l-83.7 169.7c-3.5 7.1-10.3 12-18.1 13.1l-187.3 27.2c-15.9 2.3-30.8 9.9-42 21.4l-2.7 2.9-0.3 0.3c-25.4 29.3-23.5 73.5 4.3 100.6L252.7 602c5.7 5.5 8.2 13.5 6.9 21.2l-32 186.6c-2.7 15.8-0.1 32.4 7.4 46.6l1.8 3.3 0.2 0.3c13.7 22.8 38 35.6 63 35.6 11.5 0 23.2-2.7 34.1-8.4l167.5-88.1c7-3.7 15.3-3.7 22.3 0l167.5 88.1c14.2 7.5 30.8 10.1 46.6 7.4l3.6-0.7 0.3-0.1c38-8.7 62.6-45.6 56-84l-32-186.5c-1.3-7.8 1.2-15.7 6.9-21.2L908.3 470c11.5-11.2 19.1-26.1 21.4-42l0.4-3.6v-0.4c3.7-38.9-23.8-73.7-62.4-79.3zM341.8 603.8c2.2-12.6-2-25.5-11.2-34.4l-135.5-132c-7.1-6.9-3.2-19 6.6-20.5L389 389.7c12.7-1.8 23.6-9.8 29.3-21.3L502 198.8c4.4-8.9 17.1-8.9 21.5 0l83.7 169.6c5.7 11.5 16.6 19.4 29.3 21.3l187.2 27.2c9.8 1.4 13.8 13.5 6.6 20.5l-135.5 132c-9.2 9-13.3 21.8-11.2 34.4l32 186.4c1.7 9.8-8.6 17.3-17.4 12.6l-167.4-88c-11.3-6-24.9-6-36.2 0l-167.4 88c-8.8 4.6-19.1-2.8-17.4-12.6l32-186.4z"
:fill="isFavorite ? favoriteColor : unFavoriteColor" p-id="7177"
data-spm-anchor-id="a313x.7781069.0.i12"></path>
<path d="M632.9 548.5c-3.6-13.5-14.3-24.1-27.9-27.6-13.6-3.5-28 0.6-37.7 10.7-12 12.2-28.7 19.1-45.8 19.1h-0.2c-17 0-33.6-6.9-45.6-19-15.2-15.2-39.8-15.2-55 0-15.2 15.2-15.2 39.8 0 55 26.5 26.6 63.1 41.8 100.6 41.8h0.4c37.7 0 74.5-15.4 101.1-42.2 9.8-9.8 13.7-24.3 10.1-37.8z"
:fill="favoriteColor" p-id="7178" data-spm-anchor-id="a313x.7781069.0.i10"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'iconFavoriteState',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
isFavorite: {
type: Boolean,
default: false,
},
favoriteColor: {
type: String,
default: '#d81e06',
},
unFavoriteColor: {
type: String,
default: '#ffffff',
},
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683531657415" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="32210" :width="width" :height="height" :style="{fill: color}">
<path d="M512 12.64c-282.752 0-512 229.216-512 512 0 226.208 146.72 418.144 350.144 485.824 25.6 4.736 35.008-11.104 35.008-24.64 0-12.192-0.48-52.544-0.704-95.328-142.464 30.976-172.512-60.416-172.512-60.416-23.296-59.168-56.832-74.912-56.832-74.912-46.464-31.776 3.52-31.136 3.52-31.136 51.392 3.616 78.464 52.768 78.464 52.768 45.664 78.272 119.776 55.648 148.992 42.56 4.576-33.088 17.856-55.68 32.512-68.48-113.728-12.928-233.28-56.864-233.28-253.024 0-55.904 20-101.568 52.768-137.44-5.312-12.896-22.848-64.96 4.96-135.488 0 0 43.008-13.76 140.832 52.48 40.832-11.36 84.64-17.024 128.16-17.248 43.488 0.192 87.328 5.888 128.256 17.248 97.728-66.24 140.64-52.48 140.64-52.48 27.872 70.528 10.336 122.592 5.024 135.488 32.832 35.84 52.704 81.536 52.704 137.44 0 196.64-119.776 239.936-233.792 252.64 18.368 15.904 34.72 47.04 34.72 94.816 0 68.512-0.608 123.648-0.608 140.512 0 13.632 9.216 29.6 35.168 24.576 203.328-67.776 349.856-259.616 349.856-485.76 0-282.784-229.248-512-512-512z" p-id="32211"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconGithub',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683129100927" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3671" :width="width" :height="height" :style="{fill: color}">
<path d="M798.5152 707.9936v-77.0048c0-12.288-9.8304-22.7328-22.1184-22.7328-13.9264 0-24.1664 11.4688-24.1664 25.3952V739.328c0 8.192 6.7584 14.9504 14.9504 14.9504h83.5584c13.1072 0 24.1664-10.6496 23.7568-23.7568-0.4096-13.7216-11.6736-22.528-25.3952-22.528h-50.5856z m-502.3744-119.1936c-16.384 0-29.2864 13.9264-27.8528 30.5152 1.2288 14.5408 13.9264 25.3952 28.672 25.3952h142.9504c14.5408 0 27.2384-10.8544 28.672-25.3952 1.4336-16.5888-11.4688-30.5152-27.8528-30.5152h-144.5888z m0-231.6288h309.4528c15.7696 0 28.4672-12.9024 28.0576-28.8768-0.4096-14.9504-13.7216-27.2384-28.672-27.2384H296.7552c-14.7456 0-27.8528 11.8784-28.672 26.624-0.4096 7.9872 2.6624 15.5648 8.192 21.2992 5.3248 5.12 12.288 8.192 19.8656 8.192z m241.4592 113.0496c0-15.36-12.4928-28.0576-28.0576-28.0576H296.7552c-14.5408 0-27.2384 10.8544-28.4672 25.3952-1.4336 16.5888 11.6736 30.5152 28.0576 30.5152h213.4016c15.36 0.4096 27.8528-12.288 27.8528-27.8528z m0 0" p-id="3672"></path>
<path d="M568.5248 846.4384H248.0128c-24.1664 0-44.032-19.8656-44.032-44.032V212.1728c0-24.1664 19.8656-44.032 44.032-44.032h461.0048c24.1664 0 44.032 19.8656 44.032 44.032v223.0272c0 15.5648 11.6736 28.8768 27.0336 30.3104 17.6128 1.6384 32.3584-12.288 32.3584-29.696V212.1728c0-57.1392-46.4896-103.6288-103.424-103.6288H248.0128c-55.296 0-100.5568 43.008-103.424 98.0992v601.4976c2.8672 54.8864 48.3328 97.8944 103.424 97.8944h319.8976c14.9504 0 28.0576-10.6496 30.1056-25.3952 2.6624-18.432-11.6736-34.2016-29.4912-34.2016z m0 0" p-id="3673"></path>
<path d="M782.7456 502.1696c-111.8208 0-202.752 91.3408-201.9328 203.5712 0.8192 109.7728 90.5216 199.4752 200.4992 200.4992 112.0256 0.8192 203.5712-90.112 203.5712-201.9328-0.2048-111.616-90.7264-202.1376-202.1376-202.1376z m146.432 201.9328c0 81.5104-66.7648 147.456-148.48 146.432-78.848-1.024-143.1552-65.3312-144.384-143.9744-1.2288-81.92 64.9216-148.8896 146.432-148.8896 80.6912 0 146.432 65.536 146.432 146.432z m0 0" p-id="3674"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconHistory',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683444010648" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="15448" :width="width" :height="height" :style="{fill: color}">
<path d="M787.692308 157.538462a78.769231 78.769231 0 0 1 78.76923 78.76923v551.384616a78.769231 78.769231 0 0 1-78.76923 78.76923H236.307692a78.769231 78.769231 0 0 1-78.76923-78.76923V236.307692a78.769231 78.769231 0 0 1 78.76923-78.76923h551.384616z m0 39.384615H236.307692a39.384615 39.384615 0 0 0-39.108923 34.776615L196.923077 236.307692v551.384616a39.384615 39.384615 0 0 0 34.776615 39.108923L236.307692 827.076923h551.384616a39.384615 39.384615 0 0 0 39.108923-34.776615L827.076923 787.692308V236.307692a39.384615 39.384615 0 0 0-34.776615-39.108923L787.692308 196.923077z m-354.461539 39.384615a19.692308 19.692308 0 0 1 3.544616 39.069539L433.230769 275.692308H374.153846v472.615384H433.230769a19.692308 19.692308 0 0 1 3.544616 39.069539L433.230769 787.692308H275.692308a19.692308 19.692308 0 0 1-3.544616-39.069539L275.692308 748.307692h59.076923V275.692308H275.692308a19.692308 19.692308 0 0 1-3.544616-39.069539L275.692308 236.307692h157.538461z"
p-id="15449"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconInput',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
style="margin: auto; background: transparent; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;" :width="width" :height="height" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="0" fill="none" stroke="#e90c59" stroke-width="2"
style="animation-play-state: running; animation-delay: 0s;">
<animate attributeName="r" repeatCount="indefinite" dur="1s" values="0;50" keyTimes="0;1"
keySplines="0 0.2 0.8 1" calcMode="spline" begin="0s"
style="animation-play-state: running; animation-delay: 0s;"></animate>
<animate attributeName="opacity" repeatCount="indefinite" dur="1s" values="1;0" keyTimes="0;1"
keySplines="0.2 0 0.8 1" calcMode="spline" begin="0s"
style="animation-play-state: running; animation-delay: 0s;"></animate>
</circle>
<circle cx="50" cy="50" r="0" fill="none" stroke="#46dff0" stroke-width="2"
style="animation-play-state: running; animation-delay: 0s;">
<animate attributeName="r" repeatCount="indefinite" dur="1s" values="0;50" keyTimes="0;1"
keySplines="0 0.2 0.8 1" calcMode="spline" begin="-0.5s"
style="animation-play-state: running; animation-delay: 0s;"></animate>
<animate attributeName="opacity" repeatCount="indefinite" dur="1s" values="1;0" keyTimes="0;1"
keySplines="0.2 0 0.8 1" calcMode="spline" begin="-0.5s"
style="animation-play-state: running; animation-delay: 0s;"></animate>
</circle>
</svg>
</div>
</template>
<script>
export default {
name: 'IconLoading',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
}
},
}
</script>
<style>
.lds-ripple {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ripple div {
position: absolute;
border: 4px solid #fff;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 36px;
left: 36px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 72px;
height: 72px;
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683529041470" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="28934" :width="width" :height="height" :style="{fill: color}">
<path d="M781.28 851.36a58.56 58.56 0 0 1-58.56 58.56H301.28a58.72 58.72 0 0 1-58.56-58.56V230.4h538.56zM359.68 125.44a11.84 11.84 0 0 1 12-12h281.28a11.84 11.84 0 0 1 12 12V160H359.68zM956.8 160H734.72V125.44a81.76 81.76 0 0 0-81.76-81.76H371.68a82.08 82.08 0 0 0-81.76 81.76V160H67.2a35.36 35.36 0 0 0 0 70.56h105.12v620.8a128.96 128.96 0 0 0 128.96 128.96h421.44a128.96 128.96 0 0 0 128.96-128.96V230.4h105.12a35.2 35.2 0 0 0 35.2-35.2 34.56 34.56 0 0 0-35.2-35.2zM512 804.16a35.2 35.2 0 0 0 35.2-35.36V393.92a35.2 35.2 0 1 0-70.4 0v374.88a35.2 35.2 0 0 0 35.2 35.36m-164.32 0a35.36 35.36 0 0 0 35.36-35.36V393.92a35.36 35.36 0 1 0-70.56 0v374.88a36.32 36.32 0 0 0 35.2 35.36m328.64 0a35.36 35.36 0 0 0 35.2-35.36V393.92a35.36 35.36 0 1 0-70.56 0v374.88a35.36 35.36 0 0 0 35.36 35.36" p-id="28935"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconRemove',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683529502013" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="30771" :width="width" :height="height" :style="{fill: color}">
<path d="M410.496 768L512 869.504 613.504 768H832a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64H192a64 64 0 0 0-64 64v512a64 64 0 0 0 64 64h218.496zM512 960l-128-128H192a128 128 0 0 1-128-128V192a128 128 0 0 1 128-128h640a128 128 0 0 1 128 128v512a128 128 0 0 1-128 128h-192l-128 128z" p-id="30772"></path>
<path d="M672 320h-320V256h320v64zM480 640V320h64v320h-64z" p-id="30773"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconTooltip',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683017357407" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6069" :width="width" :height="height" :style="{fill: color}">
<path d="M435.58 830.832H176.882c-35.078 0-63.892-28.814-63.892-63.891V257.059c0-35.077 28.814-63.891 63.892-63.891H435.58v-63.266H176.882c-70.156 0-127.157 57.002-127.157 127.157v509.882c0 70.155 57.001 127.157 127.157 127.157H435.58v-63.266z m365.812 0h45.726c35.078 0 63.892-28.814 63.892-63.891h63.265c0 70.155-57.001 127.157-127.157 127.157h-45.726v-63.266z m-63.892 0v63.892H592.804v-63.892H737.5zM911.01 703.05V543.946h63.265v159.103H911.01z m0-222.995V320.951h63.265v159.103H911.01z m0-222.995c0-35.077-28.814-63.891-63.892-63.891h-45.726v-63.266h45.726c70.156 0 127.157 57.002 127.157 127.157H911.01zM737.5 193.168H592.804v-63.266H737.5v63.266zM486.944 66.01h63.892v891.978h-63.892V66.011z m0 0" p-id="6070"></path>
<path d="M272.093 341.622h55.748l114.003 341.382h-51.99L362.92 597.19H237.641l-27.56 85.189H158.09l114.003-340.756z m-20.045 209.84h95.838l-46.98-149.707h-1.878l-46.98 149.708z m364.559-209.84H753.16c29.44 0 53.243 8.143 69.53 24.43 15.659 15.033 23.176 35.703 23.176 61.385 0 19.418-4.385 35.078-11.902 48.859-8.143 12.528-20.044 22.55-34.451 28.187 19.418 4.385 34.451 13.155 44.473 26.935 9.396 13.154 14.407 31.946 14.407 55.122 0 34.452-10.648 59.507-30.693 75.793-17.539 13.155-42.594 20.045-74.54 20.045H615.98V341.622z m48.858 45.726v97.717h74.54c20.672 0 35.705-4.384 44.474-11.901 8.143-8.143 13.155-21.297 13.155-38.836 0-16.286-4.385-28.188-13.155-35.078-9.395-7.517-23.176-11.275-43.22-11.275l-75.794-0.627z m0 143.444v105.86h80.805c18.165 0 33.198-3.132 43.22-10.023 13.155-8.77 20.045-21.923 20.045-41.341 0-19.419-5.011-33.2-14.407-41.969-10.022-8.769-25.682-13.154-47.606-13.154l-82.057 0.627z m0 0" p-id="6071"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconTranslate',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg t="1683436829140" viewBox="0 0 1067 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="10072" :width="width" :height="height" :style="{fill: color}">
<path d="M848.73860445 116.52039834H352.58730783a89.55799573 89.55799573 0 0 0-89.55799574 89.55799572v219.8648802a134.33699397 134.33699397 0 0 0-63.13838747 32.68866817l-8.50800957 6.7168497L123.76662828 516.84464008a16.56822936 16.56822936 0 0 0-7.16463946 12.0903293 26.8673988 26.8673988 0 0 0 3.13452995 15.22485927 19.25496877 19.25496877 0 0 0 27.31518858 3.58231974l75.67650654-58.21269765a89.55799573 89.55799573 0 0 1 81.94556572-25.52402869h118.66434495a22.38949874 22.38949874 0 0 1 21.94170895 13.43369939 30.44971853 30.44971853 0 0 1 0 20.59833887 21.04612865 21.04612865 0 0 1-21.94170895 13.4336994h-102.99169514a19.25496877 19.25496877 0 0 0 0 38.06214855H698.72896144a21.94170896 21.94170896 0 0 1 21.94170896 13.4336994 21.04612865 21.04612865 0 0 1 0 10.29916942v10.29916945a21.94170896 21.94170896 0 0 1-21.94170896 13.43369939H469.46049212a19.25496877 19.25496877 0 0 0 0 38.06214856h71.64639705a23.28507907 23.28507907 0 0 1 22.38949874 13.4336994 21.04612865 21.04612865 0 0 1 0 10.29916943v10.29916943a23.28507907 23.28507907 0 0 1-22.38949874 13.88148918H471.25165199a19.25496877 19.25496877 0 0 0-19.25496877 18.807179 19.70275932 19.70275932 0 0 0 19.25496877 18.80717978h61.34722762a25.07623893 25.07623893 0 0 1 22.38949873 13.4336994v20.59833887a22.38949874 22.38949874 0 0 1-22.38949873 15.22485926H471.25165199a18.807179 18.807179 0 1 0 0 37.61435801h25.5240287a24.62844916 24.62844916 0 0 1 21.94170897 13.88148917A20.1505491 20.1505491 0 0 1 519.61296999 832.98436494a64.48175681 64.48175681 0 0 1 0 10.29916944 21.94170896 21.94170896 0 0 1-21.94170897 13.43369939H257.20804195l-116.42539453-18.807179H135.85695759a20.1505491 20.1505491 0 0 0-18.80717901 15.67264981 20.59833887 20.59833887 0 0 0 13.43369941 23.73286884l120.45550404 29.55413821h246.28448922a38.06214856 38.06214856 0 0 0 13.88148917 0h339.87259445a89.55799573 89.55799573 0 0 0 89.55799573-89.55799572V206.07839406a89.55799573 89.55799573 0 0 0-91.79694615-89.55799572z m-447.78997941 118.21655438h391.81623218a16.12043959 16.12043959 0 0 1 17.01601914 17.01601913 16.12043959 16.12043959 0 0 1-17.01601914 17.01601914H399.15746517a16.12043959 16.12043959 0 0 1-17.01601913-17.91159868 16.12043959 16.12043959 0 0 1 17.01601913-16.12043959z m0 140.15826336a16.12043959 16.12043959 0 0 1-17.01601914-17.01601914 16.12043959 16.12043959 0 0 1 15.22485927-17.46380889h393.60739205a16.12043959 16.12043959 0 0 1 17.01601914 17.46380889 16.12043959 16.12043959 0 0 1-17.01601914 17.01601914z"
p-id="10073"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'IconUse',
props: {
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#02b7fd',
},
},
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="icon-svg" :style="{width: width + 'px', height: height + 'px'}">
<svg :width="width" :height="height" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" t="1683096165139" version="1.1" p-id="5038" :style="{fill: color}">
<path v-if="type == 'parentheses'" p-id="5039" d="m214.61538,391.33624l-83.39512,0a530.19984,530.19984 0 0 0 -92.21735,299.62317c0,111.09362 34.02387,214.26386 92.21735,299.62317l83.39512,0a464.18287,464.18287 0 0 1 -109.02954,-299.62317c0,-114.05655 40.9485,-218.55846 109.02954,-299.62317zm181.43848,599.24634a464.18287,464.18287 0 0 0 109.02954,-299.62317c0,-114.05655 -40.9485,-218.55846 -109.02954,-299.62317l83.4284,0a530.19984,530.19984 0 0 1 92.18406,299.62317c0,111.09362 -34.02387,214.26386 -92.21735,299.62317l-83.39512,0l0.00001,0z" stroke="null" />
<path v-if="type == 'brackets'" stroke="null" p-id="5198" d="m39.55555,391.33333l166.1111,0l0,66.44444l-99.66666,0l0,465.11109l99.66666,0l0,66.44444l-166.1111,0l0,-597.99996zm365.44442,531.55552l0,66.44444l166.1111,0l0,-597.99996l-166.1111,0l0,66.44444l99.66666,0l0,465.11109l-99.66666,0z" />
<g v-if="type == 'braces'" stroke="null">
<path stroke="null" p-id="5405" d="m82.76543,491.92594a99.48148,99.48148 0 0 1 99.48148,-99.48148l33.16049,0l0,66.32099l-33.16049,0a33.1605,33.1605 0 0 0 -33.1605,33.1605l0,132.64198a99.48148,99.48148 0 0 1 -99.48148,99.48148l0,-66.32099a33.1605,33.1605 0 0 0 33.1605,-33.16049l0,-132.64198z" />
<path stroke="null" p-id="5406" d="m149.08642,757.20989a99.48148,99.48148 0 0 0 -99.48148,-99.48148l-33.1605,0l0,66.32099l33.1605,0a33.1605,33.1605 0 0 1 33.1605,33.16049l0,132.64198a99.48148,99.48148 0 0 0 99.48148,99.48148l0,-66.32099a33.1605,33.1605 0 0 1 -33.1605,-33.1605l0,-132.64198zm464.24691,-265.28395a99.48148,99.48148 0 0 0 -99.48148,-99.48148l-33.16049,0l0,66.32099l33.16049,0a33.1605,33.1605 0 0 1 33.1605,33.1605l0,132.64198a99.48148,99.48148 0 0 0 99.48148,99.48148l0,-66.32099a33.1605,33.1605 0 0 1 -33.1605,-33.16049l0,-132.64198z" />
<path stroke="null" p-id="5407" d="m547.01234,757.20989a99.48148,99.48148 0 0 1 99.48148,-99.48148l33.1605,0l0,66.32099l-33.1605,0a33.1605,33.1605 0 0 0 -33.1605,33.16049l0,132.64198a99.48148,99.48148 0 0 1 -99.48148,99.48148l0,-66.32099a33.1605,33.1605 0 0 0 33.1605,-33.1605l0,-132.64198z" />
</g>
<path v-if="increase" stroke="null" d="m805.42451,14.47744l0,464.33459l-58.04183,0l0,-464.33459l58.04183,0z" p-id="2293" />
<path stroke="null" d="m1008.57088,217.62383l0,58.04183l-464.33459,0l0,-58.04183l464.33459,0z" p-id="2294" />
</svg>
</div>
</template>
<script>
export default {
name: 'IconWeight',
props: {
type: {
type: String,
/**
* parentheses: ()
* brackets: []
* braces: {}
*/
default: 'parentheses',
},
increase: {
type: Boolean,
default: true,
},
width: {
type: Number,
default: 16,
},
height: {
type: Number,
default: 16,
},
color: {
type: String,
default: '#000',
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,335 @@
<template>
<div class="physton-prompt-translate-setting" v-if="isOpen">
<div class="translate-setting-main">
<div class="setting-line">
<div class="line-title">{{ getLang('translate_api') }}</div>
<div class="line-content">
<select v-model="apiKey">
<optgroup v-for="typeGroup in supportApi" :key="typeGroup.type"
:label="getLang(typeGroup.type)">
<option v-for="item in typeGroup.children" :key="item.key" :value="item.key">
{{ item.name }}
</option>
</optgroup>
</select>
</div>
</div>
<div class="setting-line" v-if="apiItem.help">
<div class="line-title"></div>
<div class="line-content">
<div v-for="item in apiItem.help" class="help-list">
<div class="help-item">[?] <a :href="item.url" target="_blank">{{ item.title }}</a></div>
</div>
</div>
</div>
<div class="setting-line" v-for="config in configs">
<div class="line-title">{{ config.title }}</div>
<div class="line-content">
<input v-if="config.type == 'input'" v-model="config.value">
<select v-if="config.type == 'select'" v-model="config.value">
<option v-for="option in config.options" :value="option">{{ option }}</option>
</select>
</div>
</div>
<div class="setting-line">
<div class="line-title">{{ getLang('translate_test') }}</div>
<div class="line-content">
<textarea class="test-input" v-model="testText"></textarea>
</div>
</div>
<div class="setting-line">
<div class="line-title"></div>
<div class="line-content">
<div class="hover-scale-120 test-btn" @click="onTestClick">
<icon-loading v-if="loading" width="40" height="40" aria-required="true"/>
<block v-else>Test!</block>
</div>
</div>
</div>
<div class="setting-line">
<div class="line-title"></div>
<div class="line-content">
<div class="translate-error" v-if="!translateSuccess && errorMessage">{{ errorMessage }}</div>
<textarea class="test-input" v-if="translatedText" v-model="translatedText"></textarea>
</div>
</div>
<div class="setting-btns">
<div class="translate-save hover-scale-120" @click="onSaveClick">{{ getLang('save') }}</div>
<div class="translate-close hover-scale-120" @click="onCloseClick">{{ getLang('close') }}</div>
</div>
</div>
</div>
</template>
<script>
import LanguageMixin from "@/mixins/languageMixin";
import IconCopy from "@/components/icons/iconCopy.vue";
import IconLoading from "@/components/icons/iconLoading.vue";
import common from "@/utils/common";
export default {
name: 'TranslateSetting',
components: {IconLoading, IconCopy},
mixins: [LanguageMixin],
props: {},
data() {
return {
testText: `Hi, this extension is developed by Physton. Welcome to use it!
If you have any suggestions or opinions, please feel free to raise an issue or PR on Github.
If you find this extension helpful, please give me a star on Github!
You could also buy me a coffee: https://github.com/Physton/sd-webui-prompt-all-in-one#donate
Developed by: https://www.physton.com
Github: https://github.com/Physton/sd-webui-prompt-all-in-one`,
translateSuccess: false,
errorMessage: '',
translatedText: '',
loading: false,
isOpen: false,
configs: [],
apiKey: '',
}
},
computed: {
apiItem() {
return common.getTranslateApiItem(this.translateApis, this.apiKey)
},
supportApi() {
if (!this.translateApis || this.translateApis.length <= 0) return []
let api = JSON.parse(JSON.stringify(this.translateApis))
api.forEach(group => {
group.children = group.children.filter(item => item.support[this.languageCode])
})
return api
},
},
mounted() {
this.translateSuccess = false
this.errorMessage = ''
this.translatedText = ''
this.loading = false
},
watch: {
apiKey: {
handler: function (val, oldVal) {
this.translateSuccess = false
this.errorMessage = ''
this.translatedText = ''
this.loading = false
this.configs = []
this.gradioAPI.getData('translate_api.' + this.apiKey).then(res => {
for (const item of this.apiItem.config) {
if (res) {
item.value = res[item.key]
} else {
item.value = item.default || ''
}
this.configs.push(item)
}
console.log(this.configs)
})
},
},
immediate: true
},
methods: {
open(apiKey) {
this.apiKey = apiKey
this.isOpen = true
},
onTestClick() {
if (this.loading) return
this.translateSuccess = false
this.errorMessage = ''
this.translatedText = ''
this.loading = true
let configs = {}
for (const item of this.configs) {
configs[item.key] = item.value
}
this.translate(this.testText, 'en_US', this.languageCode, this.apiKey, configs).then(res => {
if (!res.success) {
this.errorMessage = res.message
} else {
this.translatedText = res.translated_text
this.translateSuccess = true
}
this.loading = false
}).catch(err => {
this.errorMessage = err.message
this.loading = false
})
},
onSaveClick() {
this.isOpen = false
let configs = {}
for (const item of this.configs) {
configs[item.key] = item.value
}
this.$emit('update:translateApi', this.apiKey)
this.gradioAPI.setData('translate_api.' + this.apiKey, configs)
},
onCloseClick() {
this.isOpen = false
}
},
}
</script>
<style lang="less">
.physton-prompt-translate-setting {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
.translate-setting-main {
width: 700px;
height: auto;
padding: 20px;
margin: 0;
box-shadow: 0 0 3px 0 #4a54ff;
border-radius: 6px 6px 4px 4px;
background-color: rgba(30, 30, 30, .9);
transition: height .1s ease-in-out, width .1s ease-in-out;
color: #fff;
.setting-line {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin-bottom: 10px;
.line-title {
font-size: 16px;
font-weight: bold;
color: #fff;
width: 150px;
line-height: 24px;
}
.line-content {
flex: 1;
font-size: 14px;
color: #fff;
span {
font-size: 14px;
color: #fff;
}
.api-name {
display: inline-block;
background: center center #4A54FF;
background-image: linear-gradient(315deg, #6772FF 0, #00F9E5 100%);
background-size: 104% 104%;
color: #1d1d1d;
border-radius: 2px;
padding: 4px;
}
input, textarea, select {
background: rgba(30, 30, 30, .9);
border: 1px solid #3c3c3c;
padding: 4px;
width: 100%;
font-size: 14px;
color: #fff;
resize: none;
&:focus {
outline: none;
border-color: #4A54FF;
}
}
.test-input {
height: 150px;
}
.test-btn {
cursor: pointer;
display: inline-block;
padding: 0 40px;
height: 40px;
line-height: 40px;
color: #fff;
background: #108bb5;
border-radius: 4px;
display: inline-block;
}
.translate-error {
color: #ff4a4a;
font-size: 14px;
margin-bottom: 10px;
}
.help-list {
font-size: 14px;
line-height: 24px;
.help-item {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
a {
color: #fff;
text-decoration: none;
font-size: 14px;
border-bottom: 1px solid #fff;
padding-bottom: 4px;
&:hover {
color: #108bb5;
border-bottom-color: #108bb5;
}
}
}
}
}
}
.setting-btns {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 10px;
.translate-save {
cursor: pointer;
display: inline-block;
padding: 0 40px;
height: 40px;
line-height: 40px;
color: #fff;
background: center center #4A54FF;
background-image: linear-gradient(315deg, #6772FF 0, #00F9E5 100%);
background-size: 104% 104%;
border-radius: 4px;
display: inline-block;
margin-right: 10px;
}
.translate-close {
cursor: pointer;
display: inline-block;
padding: 0 40px;
height: 40px;
line-height: 40px;
color: #4A54FF;
background: transparent;
border: 1px solid #4A54FF;
border-radius: 4px;
display: inline-block;
}
}
}
}
</style>

39
src/src/main.js Executable file
View File

@@ -0,0 +1,39 @@
import * as Vue from 'vue'
import App from './App.vue'
import toastr from 'toastr'
import VueClipboard from 'vue-clipboard3'
const {toClipboard} = VueClipboard()
import CommonMixin from "@/mixins/commonMixin";
import tippy from "tippy.js";
onUiLoaded(() => {
const div = document.createElement('div')
div.id = 'physton-prompt-all-in-one'
document.body.appendChild(div)
const app = Vue.createApp(App)
app.config.globalProperties.$toastr = toastr
app.config.globalProperties.$copyText = toClipboard
app.config.globalProperties.$tippyList = []
app.mixin(CommonMixin)
app.directive('tooltip', {
mounted(el, binding) {
app.config.globalProperties.$tippyList.push(tippy(el, {
content: binding.value,
placement: 'bottom',
theme: 'light',
allowHTML: true,
onCreate(instance, partialProps) {
const enable = localStorage.getItem('phystonPromptEnableTooltip') === 'true'
if (!enable) {
instance.disable()
}
},
}))
},
})
app.mount('#physton-prompt-all-in-one')
})

View File

@@ -0,0 +1,16 @@
import common from "@/utils/common";
import GradioAPI from "@/utils/gradioAPI";
export default {
data() {
return {
/**
* @type {GradioAPI}
*/
gradioAPI: null,
}
},
beforeMount() {
this.gradioAPI = new GradioAPI()
}
}

View File

@@ -0,0 +1,163 @@
import common from "@/utils/common";
export default {
props: {
languageCode: {
type: String,
default: 'en_US'
},
languages: {
type: Object,
default: () => {
},
},
translateApis: {
type: Array,
default: () => [],
},
translateApi: {
type: String,
default: '',
},
translateApiConfig: {
type: Object
},
},
data() {
return {}
},
methods: {
getLang(key) {
return common.getLang(key, this.languageCode, this.languages)
},
_translateRes(success, message, text, translated_text, from_lang, to_lang, api, apiConfig) {
return {
success,
message,
text,
translated_text,
from_lang,
to_lang,
api,
api_config: apiConfig
}
},
translate(text, from_lang, to_lang, translateApi = null, translateApiConfig = null) {
return new Promise((resolve, reject) => {
translateApi = translateApi || this.translateApi
translateApiConfig = translateApiConfig || this.translateApiConfig || {}
if (!common.canTranslate(text)) {
resolve(this._translateRes(true, '', text, text, from_lang, to_lang, translateApi, translateApiConfig))
}
if (translateApi === 'openai') {
text = JSON.stringify({text})
}
this.gradioAPI.translate(text, from_lang, to_lang, translateApi, translateApiConfig).then(res => {
if (res.success) {
if (translateApi === 'openai') {
let translated_text = res.translated_text
// 找到第一个[,截取到最后一个]然后再转成json
const start = translated_text.indexOf('{')
const end = translated_text.lastIndexOf('}')
translated_text = translated_text.substring(start, end + 1)
try {
translated_text = JSON.parse(translated_text).text
res.translated_text = translated_text
} catch (e) {
reject(e)
return
}
}
resolve(res)
} else {
reject(res)
}
}).catch(error => {
reject(error)
})
})
},
async translateMulti(texts, from_lang, to_lang, callback, complete = null, translateApi = null, translateApiConfig = null) {
translateApi = translateApi || this.translateApi
translateApiConfig = translateApiConfig || this.translateApiConfig || {}
if (translateApi === 'openai') {
let needTranslateTexts = []
for (const index in texts) {
const text = texts[index]
if (common.canTranslate(text)) {
needTranslateTexts.push({
"text": text,
"index": index
})
} else {
callback(this._translateRes(true, '', text, text, from_lang, to_lang, translateApi, translateApiConfig), index)
}
}
if (needTranslateTexts.length === 0) {
if (complete) complete()
return
}
let errors = (message) => {
for (const item of needTranslateTexts) {
callback(this._translateRes(false, message, item.text, '', from_lang, to_lang, translateApi, translateApiConfig), item.index)
}
}
this.gradioAPI.translate(JSON.stringify(needTranslateTexts), from_lang, to_lang, translateApi, translateApiConfig).then(res => {
if (res.success) {
console.log(res.translated_text)
let translated_text = res.translated_text
const start = translated_text.indexOf('[')
const end = translated_text.lastIndexOf(']')
translated_text = translated_text.substring(start, end + 1)
try {
translated_text = JSON.parse(translated_text)
for (const index in translated_text) {
const item = translated_text[index]
callback(this._translateRes(true, '', needTranslateTexts[index].text, item.text, from_lang, to_lang, translateApi, translateApiConfig), item.index)
}
} catch (e) {
errors(e.message)
}
} else {
errors(res.message)
}
if (complete) complete()
}).catch(error => {
errors(error.message)
if (complete) complete()
})
} else if (translateApiConfig.concurrent && translateApiConfig.concurrent >= texts.length) {
let completeCount = texts.length
const completeFunc = () => {
completeCount--
if (completeCount === 0) {
if (complete) complete()
}
}
// 如果并发数大于等于需要翻译的文本数,并发翻译
for (const index in texts) {
const text = texts[index]
this.translate(text, from_lang, to_lang, translateApi, translateApiConfig).then(res => {
callback(res, index)
completeFunc()
}).catch(error => {
callback(this._translateRes(false, error.message, text, '', from_lang, to_lang, translateApi, translateApiConfig), index)
completeFunc()
})
}
} else {
// 一个个翻译
for (const index in texts) {
const text = texts[index]
try {
let res = (await this.translate(text, from_lang, to_lang, translateApi, translateApiConfig))
callback(res, index)
} catch (error) {
callback(this._translateRes(false, error.message, text, '', from_lang, to_lang, translateApi, translateApiConfig), index)
}
}
if (complete) complete()
}
}
}
}

256
src/src/utils/common.js Normal file
View File

@@ -0,0 +1,256 @@
export default {
weightNumRegex: /:([0-9\.]+)/,
/**
* 分割标签
* @param tags {string}
* @returns {string[]}
*/
splitTags(tags) {
if (tags === null || tags === undefined || tags === false || tags === "") return []
// 替换
tags = tags.replace(//g, ',') // 中文逗号
tags = tags.replace(/。/g, ',') // 中文句号
tags = tags.replace(/、/g, ',') // 中文顿号
tags = tags.replace(//g, ',') // 中文分号
tags = tags.replace(//g, ',') // 日文句号
tags = tags.replace(/;/g, ',') // 英文分号
tags = tags.replace(/\t/g, ',') // 制表符
tags = tags.replace(/\n/g, ',') // 换行符
tags = tags.replace(/\r/g, ',') // 回车符
// 分割
tags = tags.split(',')
let list = []
for (let tag of tags) {
tag = tag.trim()
if (tag === '') continue
list.push(tag)
}
return list
},
/**
* 是否可以翻译
* @param text {string}
* @returns {boolean}
*/
canTranslate(text) {
// 如果<>包裹,不翻译
if (text[0] === '<' && text[text.length - 1] === '>') return false
// 如果是数字、标点符号,不翻译
if (/^[0-9,.\s!"#$%&'()*+,-./:;<=>?@\[\]^_`{|}~]+$/.test(text)) return false
// 如果是单个英文字母,不翻译
if (/^[a-zA-Z]$/.test(text)) return false
return true
},
/**
* 判断是否是英文
* @param text {string}
* @returns {boolean}
*/
isEnglish(text) {
const length = text.length
if (text[0] === '<' && text[length - 1] === '>') {
return true
}
// 通过ascii码判断
for (let i = 0; i < length; i++) {
if (text.charCodeAt(i) > 127) {
return false
}
}
return true
},
/**
* 获取语言
* @param key {string}
* @param languageCode {string}
* @param languages {object}
* @returns {string}
*/
getLang(key, languageCode, languages) {
if (languages[languageCode] && languages[languageCode].lang && languages[languageCode].lang[key]) {
return languages[languageCode].lang[key]
} else if (languages['en_US'] && languages['en_US'].lang && languages['en_US'].lang[key]) {
return languages['en_US'].lang[key]
} else {
return key
}
},
/**
* 实体化html
* @param str {string}
* @returns {string}
*/
escapeHtml(str) {
return str.replace(/[&<>'"]/g, tag => {
const chars = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;',
}
return chars[tag] || tag
})
},
/**
* 反实体化html
* @param str {string}
* @returns {string}
*/
unescapeHtml(str) {
return str.replace(/&amp;|&lt;|&gt;|&#39;|&quot;/g, tag => {
const chars = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#39;': "'",
'&quot;': '"',
}
return chars[tag] || tag
})
},
/**
* 获取标签的权重数
* @param tag {string}
* @returns {number}
*/
getTagWeightNum(tag) {
const match = tag.match(this.weightNumRegex)
let weightNum = match ? parseFloat(match[1]) : 0
weightNum = weightNum >= 0 ? weightNum : 0
return weightNum
},
/**
* 获取标签的加权数
* @param tag {string}
* @returns {number}
*/
getTagIncWeight(tag) {
return this.countLayers(tag, '(', ')')
},
/**
* 获取标签的减权数
* @param tag {string}
* @returns {number}
*/
getTagDecWeight(tag) {
return this.countLayers(tag, '[', ']')
},
/**
* 计算字符串包裹的层数
* @param str {string}
* @param start {string}
* @param end {string}
* @returns {number}
*/
countLayers(str, start = '(', end = ')') {
let count = 0
if (str.length < 2) return count // 长度小于2不可能有会有包裹
while (true) {
// 取出第一个和最后一个字符
let first = str[0]
let last = str[str.length - 1]
if (first === start && last === end) {
// 如果第一个和最后一个字符是括号且是对应的括号那么层数加1然后去掉第一个和最后一个字符
count++
// 去掉第一个和最后一个字符
str = str.slice(1, str.length - 1)
} else {
break
}
}
return count
},
/**
* 设置字符串的包裹
* @param str {string}
* @param num {number}
* @param start {string}
* @param end {string}
* @returns {string}
*/
setLayers(str, num = 0, start = '(', end = ')') {
// 先去除所有的括号
while (true) {
let first = str[0]
let last = str[str.length - 1]
if (first === start && last === end) {
// 如果第一个和最后一个字符是括号且是对应的括号那么层数加1然后去掉第一个和最后一个字符
// 去掉第一个和最后一个字符
str = str.slice(1, str.length - 1)
} else {
break
}
}
// 如果层数为0那么直接返回
if (num === 0) return str
// 如果层数大于0那么在字符串的前面加上num个start后面加上num个end
return start.repeat(num) + str + end.repeat(num)
},
/**
* 获取翻译api信息
* @param key {string}
* @param api {object}
* @returns {boolean}
*/
getTranslateApiInfo(key, api) {
let find = false
for (const group in api) {
for (const item in group.children) {
if (item.key === key) {
find = item
break
}
}
}
return find
},
/**
* 判断标签是否相等
* @param tags1 {Array}
* @param tags2 {Array}
* @param ignores {Array}
* @returns {boolean}
*/
isEqualTags(tags1, tags2, ignores = []) {
if (tags2.length !== tags1.length) return false
for (let i = 0; i < tags1.length; i++) {
for (let key in tags1[i]) {
if (ignores.includes(key)) continue
if (tags2[i][key] !== tags1[i][key]) return false
}
for (let key in tags2[i]) {
if (ignores.includes(key)) continue
if (tags2[i][key] !== tags1[i][key]) return false
}
}
return true
},
getTranslateApiItem(translateApis, translateApi) {
if (!translateApis || translateApis.length <= 0) return {}
for (let group of translateApis) {
for (let item of group.children) {
if (item.key === translateApi) {
return item
}
}
}
return {}
}
}

149
src/src/utils/favorite.js Normal file
View File

@@ -0,0 +1,149 @@
import FavoriteItem from "./favoriteItem";
// not use废弃
// not use废弃
// not use废弃
class Favorite {
/**
* 列表
* @type {Array.<FavoriteItem>}
*/
favorites = []
/**
* 构造函数
*/
constructor() {
}
/**
* 添加收藏
* @param favoriteItem {FavoriteItem}
* @returns {number}
*/
push(favoriteItem) {
this.favorites.push(favoriteItem)
return this.favorites.length - 1
}
/**
* 添加收藏
* @param favoriteItem {Object}
* @returns {number}
*/
pushObject(favoriteItem) {
return this.push(FavoriteItem.fromObject(favoriteItem))
}
/**
* 添加收藏
* @param favoriteItem {string}
* @returns {number}
*/
pushString(favoriteItem) {
return this.push(FavoriteItem.fromString(favoriteItem))
}
/**
* 设置收藏
* @param index {number}
* @param favoriteItem {FavoriteItem}
*/
setItemByIndex(index, favoriteItem) {
this.favorites[index] = favoriteItem
}
/**
* 设置收藏
* @param id {string}
* @param favoriteItem {FavoriteItem}
*/
setItemById(id, favoriteItem) {
this.setItemByIndex(this.getFavoriteIndexById(id), favoriteItem)
}
/**
* 删除收藏
* @param index {number}
*/
removeItem(index) {
this.favorites.splice(index, 1)
}
/**
* 删除收藏
* @param id {string}
*/
removeItemById(id) {
this.removeItem(this.getFavoriteIndexById(id))
}
/**
* 获取最后一条收藏
* @returns {FavoriteItem}
*/
getLatest() {
return this.favorites[this.favorites.length - 1]
}
/**
* 获取收藏
* @param id {string}
* @returns {FavoriteItem}
*/
getFavoriteById(id) {
return this.favorites.find(item => item.id === id)
}
/**
* 获取收藏
* @param index {number}
* @returns {FavoriteItem}
*/
getFavoriteByIndex(index) {
return this.favorites[index]
}
/**
* 获取收藏
* @param id {string}
* @returns {number}
*/
getFavoriteIndexById(id) {
return this.favorites.findIndex(item => item.id === id)
}
/**
* 获取所有收藏
* @returns {Array<FavoriteItem>}
*/
getFavorites() {
return this.favorites
}
/**
* 获取收藏数量
* @returns {number}
*/
getFavoritesLength() {
return this.favorites.length
}
/**
* 转换为收藏
* @returns {{name: string, id: number, time: number, prompt: string, tags: *[]}[]}
*/
toObject() {
return this.favorites.map(item => item.toObject())
}
/**
* 转换为字符串
* @returns {string}
*/
toString() {
return JSON.stringify(this.toObject())
}
}
export default Favorite

View File

@@ -0,0 +1,138 @@
// not use废弃
// not use废弃
// not use废弃
class FavoriteItem {
/**
* id
* @type {string}
*/
id = ''
/**
* 名称
* @type {string}
*/
name = ''
/**
* 标签
* @type {[]}
*/
tags = []
/**
* 提示词
* @type {string}
*/
prompt = ''
/**
* 时间
* @type {number}
*/
time = 0
constructor(id = '') {
this.id = id ? id : Math.random().toString(36).slice(2)
}
/**
* 设置名称
* @param name {string}
* @returns {FavoriteItem}
*/
setName(name) {
this.name = name
return this
}
/**
* 设置标签
* @param tags {Array}
* @returns {FavoriteItem}
*/
setTags(tags) {
this.tags = tags
return this
}
/**
* 设置提示
* @param prompt {string}
* @returns {FavoriteItem}
*/
setPrompt(prompt) {
this.prompt = prompt
return this
}
/**
* 设置时间
* @param time {number}
* @returns {FavoriteItem}
*/
setTime(time = 0) {
this.time = time || Date.now()
return this
}
/**
* 判断标签是否相等
* @param tags {Array}
* @param ignoreId {boolean}
* @returns {boolean}
*/
isEqualTags(tags, ignoreId = false) {
if (this.tags.length !== tags.length) return false
for (let i = 0; i < tags.length; i++) {
for (let key in tags[i]) {
if (ignoreId && key === 'id') continue
if (this.tags[i][key] !== tags[i][key]) return false
}
for (let key in this.tags[i]) {
if (ignoreId && key === 'id') continue
if (this.tags[i][key] !== tags[i][key]) return false
}
}
return true
}
/**
* 转换为对象
* @returns {{name: string, id: number, time: number, prompt: string, tags: *[]}}
*/
toObject() {
return {
id: this.id,
name: this.name,
tags: this.tags,
prompt: this.prompt,
time: this.time,
}
}
/**
* 转换为字符串
* @returns {string}
*/
toString() {
return JSON.stringify(this.toObject())
}
/**
* 从对象创建
* @param obj {Object}
* @returns {FavoriteItem}
*/
static fromObject(obj) {
return new FavoriteItem(obj.id || '').setName(obj.name || '').setTags(obj.tags || []).setPrompt(obj.prompt || '').setTime(obj.time || 0)
}
/**
* 从字符串创建
* @param str {string}
* @returns {FavoriteItem}
*/
static fromString(str) {
return FavoriteItem.fromObject(JSON.parse(str))
}
}
export default FavoriteItem

136
src/src/utils/gradioAPI.js Executable file
View File

@@ -0,0 +1,136 @@
import axios from "axios"
import common from "@/utils/common";
export default class GradioAPI {
apiBaseURL = "";
constructor() {
let url
if (typeof gradioURL === "string" && gradioURL !== "") {
url = new URL(gradioURL)
url = url.origin
} else {
url = window.location.origin
}
this.apiBaseURL = url + '/physton_prompt/'
this.api = axios.create({
baseURL: this.apiBaseURL,
timeout: 60000,
headers: {
"Content-Type": "application/json",
},
});
}
async getVersion() {
return (await this.api.get("/get_version")).data.version
}
async getConfig() {
return (await this.api.get("/get_config")).data
}
async getExtensions() {
return (await this.api.get("/get_extensions")).data.extensions
}
async tokenCounter(text, steps) {
return (await this.api.post("/token_counter", {text, steps})).data
}
async getData(key) {
return (await this.api.get("/get_data", {params: {key}})).data.data
}
async getDatas(keys) {
if (typeof keys === "object") {
keys = keys.join(",")
}
return (await this.api.get("/get_datas", {params: {keys}})).data.datas
}
async setData(key, data) {
return (await this.api.post("/set_data", {key, data})).data.success
}
async setDatas(datas) {
return (await this.api.post("/set_datas", {datas})).data.success
}
async getDataListItem(key, index) {
return (await this.api.get("/get_data_list_item", {params: {key, index}})).data.item
}
async pushDataList(key, item) {
return (await this.api.post("/push_data_list", {key, item})).data.success
}
async popDataList(key) {
return (await this.api.post("/pop_data_list", {key})).data.item
}
async shiftDataList(key) {
return (await this.api.post("/shift_data_list", {key})).data.item
}
async removeDataList(key, index) {
return (await this.api.post("/remove_data_list", {key, index})).data.success
}
async clearDataList(key) {
return (await this.api.post("/clear_data_list", {key})).data.success
}
async getHistories(type) {
return (await this.api.get("/get_histories", {params: {type}})).data.histories
}
async getFavorites(type) {
return (await this.api.get("/get_favorites", {params: {type}})).data.favorites
}
async pushHistory(type, tags, prompt, name = '') {
return (await this.api.post("/push_history", {type, tags, prompt, name})).data.success
}
async getLatestHistory(type) {
return (await this.api.get("/get_latest_history", {params: {type}})).data.history
}
async setHistory(type, id, tags, prompt, name) {
return (await this.api.post("/set_history", {type, id, tags, prompt, name})).data.success
}
async setHistoryName(type, id, name) {
return (await this.api.post("/set_history_name", {type, id, name})).data.success
}
async setFavoriteName(type, id, name) {
return (await this.api.post("/set_favorite_name", {type, id, name})).data.success
}
async doFavorite(type, id) {
return (await this.api.post("/dofavorite", {type, id})).data.success
}
async unFavorite(type, id) {
return (await this.api.post("/unfavorite", {type, id})).data.success
}
async deleteHistory(type, id) {
return (await this.api.post("/delete_history", {type, id})).data.success
}
async deleteHistories(type) {
return (await this.api.post("/delete_histories", {type})).data.success
}
async translate(text, from_lang, to_lang, api, api_config = {}) {
let data = (await this.api.post("/translate", {text, from_lang, to_lang, api, api_config})).data
if (data.translated_text) {
// 实体转义
data.translated_text = common.unescapeHtml(data.translated_text)
}
return data
}
}

177
src/src/utils/history.js Normal file
View File

@@ -0,0 +1,177 @@
import HistoryItem from './historyItem'
// not use废弃
// not use废弃
// not use废弃
export class History {
/**
* 最大历史记录数
* @type {number}
*/
max = 100
/**
* 列表
* @type {Array.<FavoriteItem>}
*/
histories = []
/**
* 构造函数
* @param max {number}
*/
constructor(max = 100) {
this.max = max
}
/**
* 添加历史记录
* @param historyItem {HistoryItem}
* @returns {number}
*/
push(historyItem) {
this.histories.push(historyItem)
if (this.histories.length > this.max) {
this.histories.shift()
}
return this.histories.length - 1
}
/**
* 添加历史记录
* @param historyItem {Object}
* @returns {number}
*/
pushObject(historyItem) {
return this.push(HistoryItem.fromObject(historyItem))
}
/**
* 添加历史记录
* @param historyItem {string}
* @returns {number}
*/
pushString(historyItem) {
return this.push(HistoryItem.fromString(historyItem))
}
/**
* 设置历史记录
* @param index {number}
* @param historyItem {HistoryItem}
*/
setItemByIndex(index, historyItem) {
this.histories[index] = historyItem
}
/**
* 设置历史记录
* @param id {string}
* @param historyItem {HistoryItem}
*/
setItemById(id, historyItem) {
this.setItemByIndex(this.getHistoryIndexById(id), historyItem)
}
/**
* 删除历史记录
* @param index {number}
*/
removeItem(index) {
this.histories.splice(index, 1)
}
/**
* 删除历史记录
* @param id {string}
*/
removeItemById(id) {
this.removeItem(this.getHistoryIndexById(id))
}
/**
* 设置最大历史记录数
* @param max {number}
* @returns {History}
*/
setMax(max) {
this.max = max
return this
}
/**
* 获取最大历史记录数
* @returns {number}
*/
getMax() {
return this.max
}
/**
* 获取最后一条历史记录
* @returns {FavoriteItem}
*/
getLatest() {
return this.histories[this.histories.length - 1]
}
/**
* 获取历史记录
* @param id {string}
* @returns {FavoriteItem}
*/
getHistoryById(id) {
return this.histories.find(item => item.id === id)
}
/**
* 获取历史记录
* @param index {number}
* @returns {FavoriteItem}
*/
getHistoryByIndex(index) {
return this.histories[index]
}
/**
* 获取历史记录
* @param id {string}
* @returns {number}
*/
getHistoryIndexById(id) {
return this.histories.findIndex(item => item.id === id)
}
/**
* 获取所有历史记录
* @returns {Array<FavoriteItem>}
*/
getHistories() {
return this.histories
}
/**
* 获取历史记录数
* @returns {number}
*/
getHistoriesLength() {
return this.histories.length
}
/**
* 转换为对象
* @returns {{name: string, id: number, time: number, prompt: string, tags: *[]}[]}
*/
toObject() {
return this.histories.map(item => item.toObject())
}
/**
* 转换为字符串
* @returns {string}
*/
toString() {
return JSON.stringify(this.toObject())
}
}
export default History

View File

@@ -0,0 +1,138 @@
// not use废弃
// not use废弃
// not use废弃
class HistoryItem {
/**
* id
* @type {string}
*/
id = ''
/**
* 名称
* @type {string}
*/
name = ''
/**
* 标签
* @type {[]}
*/
tags = []
/**
* 提示词
* @type {string}
*/
prompt = ''
/**
* 时间
* @type {number}
*/
time = 0
constructor(id = '') {
this.id = id ? id : Math.random().toString(36).slice(2)
}
/**
* 设置名称
* @param name {string}
* @returns {HistoryItem}
*/
setName(name) {
this.name = name
return this
}
/**
* 设置标签
* @param tags {Array}
* @returns {HistoryItem}
*/
setTags(tags) {
this.tags = tags
return this
}
/**
* 设置提示
* @param prompt {string}
* @returns {HistoryItem}
*/
setPrompt(prompt) {
this.prompt = prompt
return this
}
/**
* 设置时间
* @param time {number}
* @returns {HistoryItem}
*/
setTime(time=0) {
this.time = time || Date.now()
return this
}
/**
* 判断标签是否相等
* @param tags {Array}
* @param ignoreId {boolean}
* @returns {boolean}
*/
isEqualTags(tags, ignoreId = false) {
if (this.tags.length !== tags.length) return false
for (let i = 0; i < tags.length; i++) {
for (let key in tags[i]) {
if (ignoreId && key === 'id') continue
if (this.tags[i][key] !== tags[i][key]) return false
}
for (let key in this.tags[i]) {
if (ignoreId && key === 'id') continue
if (this.tags[i][key] !== tags[i][key]) return false
}
}
return true
}
/**
* 转换为对象
* @returns {{name: string, id: number, time: number, prompt: string, tags: *[]}}
*/
toObject() {
return {
id: this.id,
name: this.name,
tags: this.tags,
prompt: this.prompt,
time: this.time,
}
}
/**
* 转换为字符串
* @returns {string}
*/
toString() {
return JSON.stringify(this.toObject())
}
/**
* 从对象创建
* @param obj {Object}
* @returns {HistoryItem}
*/
static fromObject(obj) {
return new HistoryItem(obj.id || '').setName(obj.name || '').setTags(obj.tags || []).setPrompt(obj.prompt || '').setTime(obj.time || 0)
}
/**
* 从字符串创建
* @param str {string}
* @returns {HistoryItem}
*/
static fromString(str) {
return HistoryItem.fromObject(JSON.parse(str))
}
}
export default HistoryItem

45
src/vite.config.js Executable file
View File

@@ -0,0 +1,45 @@
import { fileURLToPath, URL } from 'node:url'
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
define: {
'process.env.NODE_ENV': '"production"',
},
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
// outDir: '../javascript',
// assetsDir: '../',
minify: true,
sourcemap: true,
watch: {
// https://rollupjs.org/configuration-options/#watch
},
lib: {
entry: resolve(__dirname, 'src/main.js'),
name: 'sd-webui-prompt-all-in-one',
formats: ['umd'],
},
rollupOptions: {
plugins: [
],
output: {
globals: {
},
name: "sdWebuiPromptAllInOne",
dir: '../', // 对于多文件构建,指定文件夹输出路径
format: 'umd',
chunkFileNames: 'javascript/[name].chunk.js', // 指定 chunk 文件名称
entryFileNames: 'javascript/[name].entry.js', // 指定入口文件名称
assetFileNames: 'style.[ext]'
},
},
}
})

1
style.css Executable file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
908305f9ae4a1dd882ddfe422ff7bcf42eb3cf09