🔧 chore(wip): try to hack react layout into webui

pull/29/head
倏昱 2023-04-18 19:35:46 +08:00
parent bc550ff1b5
commit 2ce8a893fa
16 changed files with 280 additions and 2312 deletions

10
.umirc.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'umi'
export default defineConfig({
routes: [{ path: '/', component: 'index' }],
npmClient: 'yarn',
mpa: {},
codeSplitting: false,
define: {
'process.env': process.env,
},
})

View File

@ -1,19 +1,12 @@
const gulp = require('gulp')
const less = require('gulp-less')
const ts = require('gulp-typescript')
const shell = require('gulp-shell')
const tsProject = ts.createProject('tsconfig.json')
gulp.task('compile', () => {
return gulp.src('src/script/**/*.ts').pipe(tsProject()).pipe(gulp.dest('javascript'))
})
gulp.task('umi-build', shell.task('yarn umi build'))
gulp.task('mv', shell.task('mv ./dist/index.js ./javascript/index.js && mv ./dist/index.css ./style.css'))
gulp.task('clean', shell.task('rm -r dist'))
gulp.task('less', () => {
return gulp.src('src/theme/*.less').pipe(less()).pipe(gulp.dest('./'))
})
gulp.task('build', gulp.parallel('compile', 'less'))
gulp.task('build', gulp.series('umi-build', 'mv', 'clean'))
gulp.task('watch', () => {
gulp.watch('src/theme/**/*', gulp.parallel('less'))
gulp.watch('src/script/**/*', gulp.parallel('compile'))
gulp.watch('src/**/*', gulp.series('build'))
})

View File

@ -1,19 +0,0 @@
"use strict";
/**
* 处理网站的 favicon 图标
*/
class FaviconHandler {
/**
* 设置网站的 favicon 图标
*/
static setFavicon() {
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/svg+xml';
link.href = 'https://gw.alipayobjects.com/zos/bmw-prod/51a51720-8a30-4430-b6c9-be5712364f04.svg';
document.getElementsByTagName('head')[0].appendChild(link);
}
}
onUiLoaded(() => {
FaviconHandler.setFavicon();
});

View File

@ -1,256 +0,0 @@
"use strict";
/**
* 转换器工具类
*/
class Converter {
/**
* 将数字四舍五入到小数点后四位
* @param value 数字
* @returns 四舍五入后的数字
*/
static round(value) {
return Math.round(value * 10000) / 10000;
}
/**
* 将字符串中的中文冒号和括号转换成英文冒号和括号
* @param srt 字符串
* @returns 转换后的字符串
*/
static convertStr(srt) {
return srt.replace(//g, ':').replace(//g, '(').replace(//g, ')');
}
/**
* 将字符串按照括号分割成数组
* @param str 字符串
* @returns 分割后的数组
*/
static convertStr2Array(str) {
// 匹配各种括号中的内容,包括括号本身
const bracketRegex = /([()<>[\]])/g;
/**
* 将字符串按照各种括号分割成数组
* @param str 字符串
* @returns 分割后的数组
*/
const splitByBracket = (str) => {
const arr = [];
let start = 0;
let depth = 0;
let match;
while ((match = bracketRegex.exec(str)) !== null) {
if (depth === 0 && match.index > start) {
arr.push(str.substring(start, match.index));
start = match.index;
}
if (match[0] === '(' || match[0] === '<' || match[0] === '[') {
depth++;
}
else if (match[0] === ')' || match[0] === '>' || match[0] === ']') {
depth--;
}
if (depth === 0) {
arr.push(str.substring(start, match.index + 1));
start = match.index + 1;
}
}
if (start < str.length) {
arr.push(str.substring(start));
}
return arr;
};
/**
* 将字符串按照逗号和各种括号分割成数组
* @param str 字符串
* @returns 分割后的数组
*/
const splitByComma = (str) => {
const arr = [];
let start = 0;
let inBracket = false;
for (let i = 0; i < str.length; i++) {
if (str[i] === ',' && !inBracket) {
arr.push(str.substring(start, i).trim());
start = i + 1;
}
else if (str[i].match(bracketRegex)) {
inBracket = !inBracket;
}
}
arr.push(str.substring(start).trim());
return arr;
};
/**
* 清洗字符串并输出数组
* @param str 字符串
* @returns 清洗后的数组
*/
const cleanStr = (str) => {
let arr = splitByBracket(str);
arr = arr.flatMap((s) => splitByComma(s));
return arr.filter((s) => s !== '');
};
return cleanStr(str)
.filter((item) => {
const pattern = /^[,\s ]+$/;
return !pattern.test(item);
})
.filter(Boolean)
.sort((a, b) => {
return a.includes('<') && !b.includes('<') ? 1 : b.includes('<') && !a.includes('<') ? -1 : 0;
});
}
/**
* 将数组转换成字符串
* @param array 数组
* @returns 转换后的字符串
*/
static convertArray2Str(array) {
const newArray = array.map((item) => {
if (item.includes('<'))
return item;
const newItem = item
.replace(/\s+/g, ' ')
.replace(/|\.\|。/g, ',')
.replace(/“||”|"|\/'/g, '')
.replace(/, /g, ',')
.replace(/,,/g, ',')
.replace(/,/g, ', ');
return Converter.convertStr2Array(newItem).join(', ');
});
return newArray.join(', ');
}
/**
* 将输入的字符串转换成特定格式的字符串
* @param input 输入的字符串
* @returns 转换后的字符串
*/
static convert(input) {
const re_attention = /\{|\[|\}|\]|[^{}[\]]+/gmu;
let text = Converter.convertStr(input);
const textArray = Converter.convertStr2Array(text);
text = Converter.convertArray2Str(textArray);
let res = [];
const curly_bracket_multiplier = 1.05;
const square_bracket_multiplier = 1 / 1.05;
const brackets = {
'{': { stack: [], multiplier: curly_bracket_multiplier },
'[': { stack: [], multiplier: square_bracket_multiplier },
};
/**
* 将指定范围内的数字乘以指定倍数
* @param start_position 起始位置
* @param multiplier 倍数
*/
function multiply_range(start_position, multiplier) {
for (let pos = start_position; pos < res.length; pos++) {
res[pos][1] = Converter.round(res[pos][1] * multiplier);
}
}
for (const match of text.matchAll(re_attention)) {
let word = match[0];
if (word in brackets) {
brackets[word].stack.push(res.length);
}
else if (word === '}' || word === ']') {
const bracket = brackets[word === '}' ? '{' : '['];
if (bracket.stack.length > 0) {
multiply_range(bracket.stack.pop(), bracket.multiplier);
}
}
else {
res.push([word, 1.0]);
}
}
Object.keys(brackets).forEach((bracketType) => {
brackets[bracketType].stack.forEach((pos) => {
multiply_range(pos, brackets[bracketType].multiplier);
});
});
if (res.length === 0) {
res = [['', 1.0]];
}
let i = 0;
while (i + 1 < res.length) {
if (res[i][1] === res[i + 1][1]) {
res[i][0] += res[i + 1][0];
res.splice(i + 1, 1);
}
else {
i += 1;
}
}
let result = '';
for (const [word, value] of res) {
result += value === 1.0 ? word : `(${word}:${value.toString()})`;
}
return result;
}
/**
* 触发 input 事件
* @param target 目标元素
*/
static dispatchInputEvent(target) {
let inputEvent = new Event('input');
Object.defineProperty(inputEvent, 'target', { value: target });
target.dispatchEvent(inputEvent);
}
/**
* 点击转换按钮的事件处理函数
* @param type 类型
*/
static onClickConvert(type) {
const default_prompt = '';
const default_negative = '';
const prompt = gradioApp().querySelector(`#${type}_prompt > label > textarea`);
const result = Converter.convert(prompt.value);
prompt.value = result.match(/^masterpiece, best quality,/) === null ? default_prompt + result : result;
Converter.dispatchInputEvent(prompt);
const negprompt = gradioApp().querySelector(`#${type}_neg_prompt > label > textarea`);
const negResult = Converter.convert(negprompt.value);
negprompt.value =
negResult.match(/^lowres,/) === null
? negResult.length === 0
? default_negative
: default_negative + negResult
: negResult;
Converter.dispatchInputEvent(negprompt);
}
/**
* 创建转换按钮
* @param id 按钮 id
* @param innerHTML 按钮文本
* @param onClick 点击事件处理函数
* @returns 新建的按钮元素
*/
static createButton(id, innerHTML, onClick) {
const button = document.createElement('button');
button.id = id;
button.type = 'button';
button.innerHTML = innerHTML;
button.title = 'Format prompt~🪄';
button.className = 'lg secondary gradio-button tool svelte-1ipelgc';
button.addEventListener('click', onClick);
return button;
}
/**
* 添加转换按钮
* @param type - 组件类型
*/
static addPromptButton(type) {
const generateBtn = gradioApp().querySelector(`#${type}_generate`);
const actionsColumn = gradioApp().querySelector(`#${type}_style_create`);
const nai2local = gradioApp().querySelector(`#${type}_formatconvert`);
if (!generateBtn || !actionsColumn || nai2local)
return;
const convertBtn = Converter.createButton(`${type}_formatconvert`, '🪄', () => Converter.onClickConvert(type));
actionsColumn.parentNode?.append(convertBtn);
}
}
/**
* 注册UI更新回调函数
* 在UI更新时添加提示按钮
*/
onUiUpdate(() => {
Converter.addPromptButton('txt2img');
Converter.addPromptButton('img2img');
});

172
javascript/index.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,63 +0,0 @@
"use strict";
class BracketChecker {
textArea;
counterElt;
errorStrings;
constructor(textArea, counterElt) {
this.textArea = textArea;
this.counterElt = counterElt;
this.errorStrings = [
{
regex: '\\(',
error: '(...) - Different number of opening and closing parentheses detected.\n',
},
{
regex: '\\[',
error: '[...] - Different number of opening and closing square brackets detected.\n',
},
{
regex: '\\{',
error: '{...} - Different number of opening and closing curly brackets detected.\n',
},
];
}
/**
* 检查文本框中的括号是否匹配并更新计数器元素的标题和样式
*/
check = () => {
let title = '';
this.errorStrings.forEach(({ regex, error }) => {
const openMatches = (this.textArea.value.match(new RegExp(regex, 'g')) || []).length;
const closeMatches = (this.textArea.value.match(new RegExp(regex.replace(/\(/g, ')').replace(/\[/g, ']').replace(/\{/g, '}'), 'g')) ||
[]).length;
if (openMatches !== closeMatches) {
if (!this.counterElt.title.includes(error)) {
title += error;
}
}
else {
title = this.counterElt.title.replace(error, '');
}
});
this.counterElt.title = title;
this.counterElt.classList.toggle('error', !!title);
};
}
/**
* 初始化括号匹配检查器
* @param id_prompt 包含文本框的元素的 ID
* @param id_counter 显示计数器的元素的 ID
*/
const setupBracketChecking = (idPrompt, idCounter) => {
const textarea = gradioApp().querySelector(`#${idPrompt} > label > textarea`);
const counter = gradioApp().getElementById(idCounter);
const bracketChecker = new BracketChecker(textarea, counter);
textarea.addEventListener('input', bracketChecker.check);
};
onUiUpdate(() => {
const elements = ['txt2img', 'txt2img_neg', 'img2img', 'img2img_neg'];
elements.forEach((prompt) => {
setupBracketChecking(`${prompt}_prompt`, `${prompt}_token_counter`);
setupBracketChecking(`${prompt}_prompt`, `${prompt}_negative_token_counter`);
});
});

View File

@ -9,7 +9,6 @@
"license": "MIT",
"author": "canisminor1990 <i@canisminor.cc>",
"sideEffects": false,
"main": "style.css",
"scripts": {
"build": "gulp build",
"dev": "gulp watch",
@ -45,25 +44,32 @@
"dependencies": {},
"devDependencies": {
"@commitlint/cli": "^17",
"@types/node": "^18",
"@types/react": "^18",
"@types/react-dom": "^18",
"@umijs/lint": "^4.0.64",
"antd": "^5.4.2",
"antd-style": "^3.0.0",
"commitlint": "^17",
"commitlint-config-gitmoji": "^2",
"concurrently": "^8.0.1",
"eslint": "^8",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^3.5.5",
"gulp": "^4.0.2",
"gulp-less": "^5.0.0",
"gulp-typescript": "^6.0.0-alpha.1",
"gulp": "^4",
"gulp-shell": "^0.8.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.1",
"object-to-css-variables": "^0.2.1",
"prettier": "^2",
"prettier-plugin-organize-imports": "^3",
"prettier-plugin-packagejson": "^2",
"query-string": "^8.1.0",
"react": "^18",
"react-dom": "^18",
"semantic-release": "^21",
"semantic-release-config-gitmoji": "^1",
"styled-components": "^5.3.9",
"stylelint": "^15.4.0",
"stylelint-less": "^1.0.6",
"typescript": "^5.0.0",

19
src/pages/index/App.tsx Normal file
View File

@ -0,0 +1,19 @@
import { ThemeProvider } from 'antd-style'
import qs from 'query-string'
import React, { useEffect, useState } from 'react'
import Layout from './Layout'
const App: React.FC = () => {
const [appearance, setAppearance] = useState<string>('auto')
useEffect(() => {
setAppearance(String(qs.parseUrl(window.location.href).query.__theme) || 'auto')
}, [])
return (
<ThemeProvider appearance={appearance}>
<Layout />
</ThemeProvider>
)
}
export default App

View File

@ -0,0 +1,17 @@
import { Layout } from 'antd'
import React from 'react'
const { Header } = Layout
const LayoutView: React.FC = () => {
// const header = gradioApp().getElementById('quicksettings')
const header = document.getElementById('quicksettings')
return (
<Layout>
<Header>{header}</Header>
</Layout>
)
}
export default LayoutView

26
src/pages/index/index.tsx Normal file
View File

@ -0,0 +1,26 @@
import { createRoot } from 'react-dom/client'
import favicon from '../../script/favicon'
import formatPrompt from '../../script/format-prompt'
import promptBracketChecker from '../../script/prompt-bracket-checker'
import '../../theme/style.less'
import App from './App'
document.addEventListener('DOMContentLoaded', () => {
const root = document.createElement('div')
root.setAttribute('id', 'root')
document.body.append(root)
const client = createRoot(root)
client.render(<App />)
})
onUiLoaded(() => {
favicon()
window.init = true
})
onUiUpdate(() => {
formatPrompt()
promptBracketChecker()
})
export default () => null

View File

@ -14,6 +14,8 @@ class FaviconHandler {
}
}
onUiLoaded(() => {
onUiLoaded(() => {})
export default () => {
FaviconHandler.setFavicon()
})
}

View File

@ -269,7 +269,9 @@ class Converter {
* UI
* UI
*/
onUiUpdate(() => {
onUiUpdate(() => {})
export default () => {
Converter.addPromptButton('txt2img')
Converter.addPromptButton('img2img')
})
}

View File

@ -63,10 +63,12 @@ const setupBracketChecking = (idPrompt: string, idCounter: string): void => {
textarea.addEventListener('input', bracketChecker.check)
}
onUiUpdate(() => {
onUiUpdate(() => {})
export default () => {
const elements = ['txt2img', 'txt2img_neg', 'img2img', 'img2img_neg']
elements.forEach((prompt) => {
setupBracketChecking(`${prompt}_prompt`, `${prompt}_token_counter`)
setupBracketChecking(`${prompt}_prompt`, `${prompt}_negative_token_counter`)
})
})
}

View File

@ -11,7 +11,7 @@
@import 'components/container';
@import 'components/scrollbar';
@import 'components/options';
@import 'components/header';
//@import 'components/header';
@import 'components/modal';
@import 'components/sliders';
@import 'components/button';
@ -44,22 +44,27 @@ code {
h1 {
font-size: var(--font-size-heading1);
line-height: var(--line-height-heading1);
color: var(--color-text);
}
h2 {
font-size: var(--font-size-heading2);
line-height: var(--line-height-heading2);
color: var(--color-text);
}
h3 {
font-size: var(--font-size-heading3);
line-height: var(--line-height-heading3);
color: var(--color-text);
}
h4 {
font-size: var(--font-size-heading4);
line-height: var(--line-height-heading4);
color: var(--color-text);
}
h5 {
font-size: var(--font-size-heading5);
line-height: var(--line-height-heading5);
color: var(--color-text);
}
/* Theme Fix */

1951
style.css

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
{
"extends": "./src/.umi-production/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,