♻️ refactor: refactor project

pull/142/head
倏昱 2023-06-21 23:54:32 +08:00
parent e116458d10
commit b9046f13a2
67 changed files with 3845 additions and 2896 deletions

View File

@ -1,16 +1,13 @@
# http://editorconfig.org # http://editorconfig.org
root = true root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
tab_width = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
max_line_length = 100
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

1
.env Normal file
View File

@ -0,0 +1 @@
VITE_CONTEXT=DEV

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_CONTEXT=PROD

View File

@ -28,3 +28,4 @@ logs
# add other ignore file below # add other ignore file below
javascript javascript
style.css style.css
vite-env.d.ts

View File

@ -1,10 +1,15 @@
const config = require('@lobehub/lint').eslint;
config.rules['indent'] = ['off', 2];
module.exports = { module.exports = {
...require('@lobehub/lint').eslint, ...config,
overrides: [ overrides: [
{ {
files: ['*.ts', '*.tsx'], files: ['*.ts', '*.tsx'],
rules: { rules: {
'no-undef': 0, 'no-undef': 0,
'object-curly-spacing': 0,
'unicorn/prefer-add-event-listener': 0, 'unicorn/prefer-add-event-listener': 0,
}, },
}, },

View File

@ -1,62 +0,0 @@
// @ts-ignore
import browserslist from 'browserslist';
// @ts-ignore
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
// @ts-ignore
import lightningcss from 'lightningcss';
import { defineConfig } from 'umi';
import WebpackShellPlugin from 'webpack-shell-plugin-next';
const mac = [
'rm ./javascript/index.js',
'rm ./style.css',
'cp ./dist/index.js ./javascript/index.js',
'cp ./dist/index.css ./style.css',
];
const win = [
'del javascript\\index.js',
'del style.css',
'copy dist\\index.js javascript\\index.js',
'copy dist\\index.css style.css',
];
export default defineConfig({
routes: [{ path: '/', component: 'index' }],
npmClient: 'pnpm',
mpa: {},
codeSplitting: false,
define: {
'process.env': process.env,
},
extraBabelPlugins: [
[
'babel-plugin-styled-components',
{
minify: true,
transpileTemplateLiterals: true,
displayName: process.env.NODE_ENV === 'development',
pure: true,
},
],
],
chainWebpack(memo) {
memo.plugin('minimizer').use(CssMinimizerPlugin, [
{
minify: CssMinimizerPlugin.lightningCssMinify,
minimizerOptions: {
targets: lightningcss.browserslistToTargets(browserslist('>= 0.25%')),
},
},
]);
memo.plugin('shell').use(WebpackShellPlugin, [
{
onBuildExit: {
scripts: process.platform === 'win32' ? win : mac,
blocking: false,
parallel: false,
},
},
]);
},
});

File diff suppressed because one or more lines are too long

1703
javascript/main.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,13 @@
"name": "sd-webui-kitchen-theme", "name": "sd-webui-kitchen-theme",
"version": "1.8.3", "version": "1.8.3",
"private": true, "private": true,
"keywords": [
"stable-diffusion-webui"
],
"homepage": "https://github.com/canisminor1990/sd-webui-kitchen-theme",
"bugs": {
"url": "https://github.com/canisminor1990/sd-webui-kitchen-theme/issues/new"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/canisminor1990/sd-webui-kitchen-theme.git" "url": "https://github.com/canisminor1990/sd-webui-kitchen-theme.git"
@ -10,8 +17,8 @@
"author": "canisminor1990 <i@canisminor.cc>", "author": "canisminor1990 <i@canisminor.cc>",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"build": "umi build", "build": "tsc && vite build",
"dev": "umi build", "dev": "vite",
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix", "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
"lint:md": "remark . --quiet --output", "lint:md": "remark . --quiet --output",
"lint:style": "stylelint \"src/**/*.{css,less,js,jsx,ts,tsx}\" --fix", "lint:style": "stylelint \"src/**/*.{css,less,js,jsx,ts,tsx}\" --fix",
@ -19,8 +26,6 @@
"prettier": "prettier -c --write \"**/**\"", "prettier": "prettier -c --write \"**/**\"",
"release": "semantic-release", "release": "semantic-release",
"sd-debug": "cd ../../ && ./webui.sh", "sd-debug": "cd ../../ && ./webui.sh",
"setup": "umi setup",
"start": "umi build",
"test": "npm run lint", "test": "npm run lint",
"type-check": "tsc -p tsconfig-check.json" "type-check": "tsc -p tsconfig-check.json"
}, },
@ -49,18 +54,17 @@
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^5", "@ant-design/icons": "^5",
"@babel/plugin-syntax-import-assertions": "^7",
"@commitlint/cli": "^17", "@commitlint/cli": "^17",
"@lobehub/lint": "latest", "@lobehub/lint": "latest",
"@lobehub/ui": "latest", "@lobehub/ui": "latest",
"@types/lodash-es": "^4", "@types/lodash-es": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dnd": "^3",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-rnd": "^8",
"@types/react-tag-input": "^6", "@types/react-tag-input": "^6",
"@types/styled-components": "^5", "@types/styled-components": "^5",
"@umijs/lint": "^4", "@vitejs/plugin-react": "^4",
"ahooks": "^3", "ahooks": "^3",
"antd": "^5", "antd": "^5",
"antd-style": "latest", "antd-style": "latest",
@ -70,6 +74,7 @@
"concurrently": "^8", "concurrently": "^8",
"css-minimizer-webpack-plugin": "^5", "css-minimizer-webpack-plugin": "^5",
"eslint": "^8", "eslint": "^8",
"fast-deep-equal": "^3",
"husky": "^8", "husky": "^8",
"lightningcss": "^1", "lightningcss": "^1",
"lint-staged": "^13", "lint-staged": "^13",
@ -79,10 +84,7 @@
"polished": "^4", "polished": "^4",
"prettier": "^2", "prettier": "^2",
"query-string": "^8", "query-string": "^8",
"re-resizable": "^6",
"react": "^18", "react": "^18",
"react-dnd": "^16",
"react-dnd-html5-backend": "^16",
"react-dom": "^18", "react-dom": "^18",
"react-layout-kit": "^1", "react-layout-kit": "^1",
"react-rnd": "^10", "react-rnd": "^10",
@ -93,9 +95,18 @@
"styled-components": "latest", "styled-components": "latest",
"stylelint": "^15", "stylelint": "^15",
"typescript": "^5", "typescript": "^5",
"umi": "^4", "vite": "^4.3.9",
"use-merge-value": "^1",
"webpack-shell-plugin-next": "^2", "webpack-shell-plugin-next": "^2",
"zustand": "^4" "zustand": "^4"
},
"peerDependencies": {
"antd": ">=5",
"antd-style": ">=3",
"react": ">=18",
"react-dom": ">=18"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
} }
} }

29
src/App.tsx Normal file
View File

@ -0,0 +1,29 @@
import { memo, useEffect, useState } from 'react';
import { shallow } from 'zustand/shallow';
import Layout from '@/layouts';
import Index from '@/pages';
import formatPrompt from '@/script/formatPrompt';
import promptBracketChecker from '@/script/promptBracketChecker';
import setupHead from '@/script/setupHead';
import { useAppStore } from '@/store';
const App = memo(() => {
const [loading, setLoading] = useState(true);
const setCurrentTab = useAppStore((st) => st.setCurrentTab, shallow);
useEffect(() => {
setupHead();
onUiLoaded(() => {
formatPrompt();
promptBracketChecker();
setLoading(false);
});
onUiUpdate(() => {
setCurrentTab();
});
}, []);
return <Layout>{!loading && <Index />}</Layout>;
});
export default App;

10
src/_react_refresh.js Normal file
View File

@ -0,0 +1,10 @@
import RefreshRuntime from '/@react-refresh';
const RefreshSig = (type) => type;
RefreshRuntime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => RefreshSig;
window.__vite_plugin_react_preamble_installed__ = true;
console.log('🚧 Injecting React Refresh');

View File

@ -1,39 +0,0 @@
import {FloatButton} from 'antd';
import {type ReactNode, memo, useRef} from 'react';
import styled from 'styled-components';
import {shallow} from 'zustand/shallow';
import {useAppStore} from '@/store';
const ContentView = styled.div<{ isPromptResizable: boolean }>`
overflow-x: hidden;
overflow-y: auto;
flex: 1;
[id$='2img_prompt'] textarea {
max-height: ${({isPromptResizable}) => (isPromptResizable ? 'unset' : '84px')};
}
[id$='2img_neg_prompt'] textarea {
max-height: ${({isPromptResizable}) => (isPromptResizable ? 'unset' : '84px')};
}
`;
interface ContentProps {
children: ReactNode;
loading?: boolean;
}
const Content = memo<ContentProps>(({children}) => {
const reference: any = useRef(null);
const [setting] = useAppStore((st) => [st.setting], shallow);
return (
<ContentView isPromptResizable={setting.promotTextarea === 'resizable'} ref={reference}>
{children}
<FloatButton.BackTop target={() => reference.current} />
</ContentView>
);
});
export default Content;

View File

@ -1,228 +0,0 @@
import {useHover} from 'ahooks';
import {ChevronDown, ChevronLeft, ChevronRight, ChevronUp} from 'lucide-react';
import type {Enable, NumberSize, Size} from 're-resizable';
import {HandleClassName, Resizable} from 're-resizable';
import {memo, useEffect, useMemo, useRef, useState} from 'react';
import {Center} from 'react-layout-kit';
import type {Props as RndProps} from 'react-rnd';
import useControlledState from 'use-merge-value';
import type {DivProps} from '@/types';
import {useStyle} from './style';
import {revesePlacement} from './utils';
const DEFAULT_HEIGHT = 180;
const DEFAULT_WIDTH = 280;
export type placementType = 'right' | 'left' | 'top' | 'bottom';
export interface DraggablePanelProps extends DivProps {
defaultExpand?: boolean;
defaultSize?: Partial<Size>;
destroyOnClose?: boolean;
expand?: boolean;
expandable?: boolean;
hanlderStyle?: React.CSSProperties;
minHeight?: number;
minWidth?: number;
mode?: 'fixed' | 'float';
onExpandChange?: (expand: boolean) => void;
onSizeChange?: (delta: NumberSize, size?: Size) => void;
onSizeDragging?: (delta: NumberSize, size?: Size) => void;
pin?: boolean;
placement: placementType;
resize?: RndProps['enableResizing'];
showHandlerWhenUnexpand?: boolean;
size?: Partial<Size>;
}
const DraggablePanel = memo<DraggablePanelProps>(
({
pin = 'true',
mode = 'fixed',
children,
placement = 'right',
resize,
style,
size,
defaultSize: customizeDefaultSize,
minWidth,
minHeight,
onSizeChange,
onSizeDragging,
expandable = true,
expand,
defaultExpand = true,
onExpandChange,
className,
showHandlerWhenUnexpand,
destroyOnClose,
hanlderStyle,
}) => {
const reference = useRef(null);
const isHovering = useHover(reference);
const isVertical = placement === 'top' || placement === 'bottom';
const {styles, cx} = useStyle('draggable-panel');
const [isExpand, setIsExpand] = useControlledState(defaultExpand, {
onChange: onExpandChange,
value: expand,
});
useEffect(() => {
if (pin) return;
if (isHovering && !isExpand) {
setIsExpand(true);
} else if (!isHovering && isExpand) {
setIsExpand(false);
}
}, [pin, isHovering, isExpand]);
const [showExpand, setShowExpand] = useState(true);
const canResizing = resize !== false && isExpand;
const resizeHandleClassNames: HandleClassName = useMemo(() => {
if (!canResizing) return {};
return {
[revesePlacement(placement)]: styles[`${revesePlacement(placement)}Handle`],
};
}, [canResizing, placement]);
const resizing = {
bottom: false,
bottomLeft: false,
bottomRight: false,
left: false,
right: false,
top: false,
topLeft: false,
topRight: false,
[revesePlacement(placement)]: true,
...(resize as Enable),
};
const defaultSize: Size = useMemo(() => {
if (isVertical) {
return {
height: DEFAULT_HEIGHT,
width: '100%',
...customizeDefaultSize,
};
}
return {
height: '100%',
width: DEFAULT_WIDTH,
...customizeDefaultSize,
};
}, [isVertical]);
const sizeProps = isExpand ?
{
defaultSize,
minHeight: typeof minHeight === 'number' ? Math.max(minHeight, 0) : undefined,
minWidth: typeof minWidth === 'number' ? Math.max(minWidth, 0) : 280,
size: size as Size,
} :
isVertical ?
{
minHeight: 0,
size: {height: 0},
} :
{
minWidth: 0,
size: {width: 0},
};
const {Arrow, className: arrowPlacement} = useMemo(() => {
switch (placement) {
case 'top': {
return {Arrow: ChevronDown, className: 'Bottom'};
}
case 'bottom': {
return {Arrow: ChevronUp, className: 'Top'};
}
case 'right': {
return {Arrow: ChevronLeft, className: 'Left'};
}
case 'left': {
return {Arrow: ChevronRight, className: 'Right'};
}
}
}, [styles, placement]);
const handler = (
// @ts-ignore
<Center
// @ts-ignore
className={cx(styles[`toggle${arrowPlacement}`])}
style={{opacity: isExpand ? (pin ? undefined : 0) : showHandlerWhenUnexpand ? 1 : 0}}
>
<Center
onClick={() => {
setIsExpand(!isExpand);
}}
style={hanlderStyle}
>
<div
className={styles.handlerIcon}
style={{transform: `rotate(${isExpand ? 180 : 0}deg)`}}
>
<Arrow size={16} strokeWidth={1.5} />
</div>
</Center>
</Center>
);
const inner = (
// @ts-ignore
<Resizable
{...sizeProps}
className={styles.panel}
enable={canResizing ? (resizing as Enable) : undefined}
handleClasses={resizeHandleClassNames}
onResize={(_, direction, reference_, delta) => {
onSizeDragging?.(delta, {
height: reference_.style.height,
width: reference_.style.width,
});
}}
onResizeStart={() => {
setShowExpand(false);
}}
onResizeStop={(e, direction, reference_, delta) => {
setShowExpand(true);
onSizeChange?.(delta, {
height: reference_.style.height,
width: reference_.style.width,
});
}}
style={style}
>
{children}
</Resizable>
);
return (
<div
className={cx(
styles.container,
// @ts-ignore
styles[mode === 'fixed' ? 'fixed' : `${placement}Float`],
className,
)}
ref={reference}
style={{[`border${arrowPlacement}Width`]: 1}}
>
{expandable && showExpand && handler}
{destroyOnClose ? isExpand && inner : inner}
</div>
);
},
);
export default DraggablePanel;

View File

@ -1,267 +0,0 @@
import {createStyles, css, cx} from 'antd-style';
const offset = 17;
const toggleLength = 40;
const toggleShort = 16;
export const useStyle = createStyles(({token}, prefix: string) => {
const commonHandle = css`
position: relative;
&::before {
content: '';
position: absolute;
z-index: 50;
transition: all 0.2s ${token.motionEaseOut};
}
&:hover,
&:active {
&::before {
background: ${token.colorPrimary};
}
}
`;
const commonToggle = css`
position: absolute;
z-index: 1001;
opacity: 0;
transition: all 0.2s ${token.motionEaseOut};
&:hover {
opacity: 1 !important;
}
&:active {
opacity: 1 !important;
}
> div {
cursor: pointer;
position: absolute;
color: ${token.colorTextTertiary};
background: ${token.colorFillTertiary};
border-color: ${token.colorBorderSecondary};
border-style: solid;
border-width: 1px;
border-radius: 4px;
transition: all 0.2s ${token.motionEaseOut};
&:hover {
color: ${token.colorTextSecondary};
background: ${token.colorFillSecondary};
}
&:active {
color: ${token.colorText};
background: ${token.colorFill};
}
}
`;
const float = css`
position: absolute;
z-index: 2000;
`;
return {
bottomFloat: cx(
float,
css`
right: 0;
bottom: 0;
left: 0;
width: 100%;
`,
),
bottomHandle: cx(
`${prefix}-bottom-handle`,
css`
${commonHandle};
&::before {
bottom: 50%;
width: 100%;
height: 2px;
}
`,
),
container: cx(
prefix,
css`
flex-shrink: 0;
border: 0 solid ${token.colorBorderSecondary};
&:hover {
.${prefix}-toggle {
opacity: 1;
}
}
`,
),
fixed: css`
position: relative;
`,
handlerIcon: css`
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ${token.motionEaseOut};
`,
leftFloat: cx(
float,
css`
top: 0;
bottom: 0;
left: 0;
height: 100%;
`,
),
leftHandle: cx(
css`
${commonHandle};
&::before {
left: 50%;
width: 2px;
height: 100%;
}
`,
`${prefix}-left-handle`,
),
panel: cx(
`${prefix}-fixed`,
css`
overflow: hidden;
background: ${token.colorBgContainer};
transition: all 0.2s ${token.motionEaseOut};
`,
),
rightFloat: cx(
float,
css`
top: 0;
right: 0;
bottom: 0;
height: 100%;
`,
),
rightHandle: cx(
css`
${commonHandle};
&::before {
right: 50%;
width: 2px;
height: 100%;
}
`,
`${prefix}-right-handle`,
),
toggleBottom: cx(
`${prefix}-toggle`,
`${prefix}-toggle-bottom`,
commonToggle,
css`
bottom: -${offset}px;
width: 100%;
height: ${toggleShort}px;
> div {
left: 50%;
width: ${toggleLength}px;
height: 16px;
margin-left: -20px;
border-radius: 0 0 4px 4px;
}
`,
),
toggleLeft: cx(
`${prefix}-toggle`,
`${prefix}-toggle-left`,
commonToggle,
css`
left: -${offset}px;
width: ${toggleShort}px;
height: 100%;
> div {
top: 50%;
width: ${toggleShort}px;
height: ${toggleLength}px;
margin-top: -20px;
border-radius: 4px 0 0 4px;
}
`,
),
toggleRight: cx(
`${prefix}-toggle`,
`${prefix}-toggle-right`,
commonToggle,
css`
right: -${offset}px;
width: ${toggleShort}px;
height: 100%;
> div {
top: 50%;
width: ${toggleShort}px;
height: ${toggleLength}px;
margin-top: -20px;
border-radius: 0 4px 4px 0;
}
`,
),
toggleTop: cx(
`${prefix}-toggle`,
`${prefix}-toggle-top`,
commonToggle,
css`
top: -${offset}px;
width: 100%;
height: ${toggleShort}px;
> div {
left: 50%;
width: ${toggleLength}px;
height: ${toggleShort}px;
margin-left: -20px;
border-radius: 4px 4px 0 0;
}
`,
),
topFloat: cx(
float,
css`
top: 0;
right: 0;
left: 0;
width: 100%;
`,
),
topHandle: cx(
`${prefix}-top-handle`,
css`
${commonHandle};
&::before {
top: 50%;
width: 100%;
height: 2px;
}
`,
),
};
});

View File

@ -1,18 +0,0 @@
import {placementType} from './index';
export const revesePlacement = (placement: placementType) => {
switch (placement) {
case 'bottom': {
return 'top';
}
case 'top': {
return 'bottom';
}
case 'right': {
return 'left';
}
case 'left': {
return 'right';
}
}
};

View File

@ -1,114 +0,0 @@
import {ZoomInOutlined} from '@ant-design/icons';
import {DraggablePanel} from '@lobehub/ui';
import {Slider} from 'antd';
import {useResponsive} from 'antd-style';
import {type CSSProperties, type ReactNode, memo, useEffect, useState} from 'react';
import styled, {createGlobalStyle} from 'styled-components';
import {shallow} from 'zustand/shallow';
import {useAppStore} from '@/store';
const GlobalStyle = createGlobalStyle`
button#txt2img_extra_networks,
button#img2img_extra_networks {
display: none !important;
}
`;
const View = styled.div`
overflow: hidden;
display: flex;
flex-direction: column;
height: var(--fill-available);
`;
const SidebarView = styled.div<{ size: number }>`
overflow-x: hidden;
overflow-y: auto;
flex: 1;
padding: 16px;
#txt2img_extra_networks,
#img2img_extra_networks {
display: block !important;
}
.extra-network-cards .actions .name {
background: unset !important;
}
.extra-network-cards,
.extra-network-thumbs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(${({size}) => size}px, 1fr));
> .card {
width: var(--fill-available) !important;
height: ${({size}) => size * 1.5}px !important;
}
}
`;
const Footer = styled.div`
display: flex;
flex: 0;
align-items: center;
justify-content: flex-start;
padding: 8px 16px;
border-top: 1px solid var(--color-border);
`;
const ZoomSlider = styled(Slider)`
flex: 1;
margin-left: 16px;
`;
interface SidebarProps {
children: ReactNode;
loading?: boolean;
style?: CSSProperties;
}
const Sidebar = memo<SidebarProps>(({children, style}) => {
const {mobile} = useResponsive();
const [setting] = useAppStore((st) => [st.setting], shallow);
const [mode] = useState<'fixed' | 'float'>(setting.extraNetworkFixedMode);
const [expand, setExpand] = useState<boolean>(setting.extraNetworkSidebarExpand);
const [size, setSize] = useState<number>(setting?.extraNetworkCardSize || 86);
useEffect(() => {
if (mobile) setExpand(false);
}, []);
return (
<>
<GlobalStyle />
<DraggablePanel
defaultSize={{width: setting.extraNetworkSidebarWidth}}
expand={expand}
minWidth={setting.extraNetworkSidebarWidth}
mode={mode}
onExpandChange={setExpand}
pin={mode === 'fixed'}
placement="right"
style={{
display: 'flex',
flexDirection: 'column',
...style,
}}
>
<View>
<SidebarView size={size}>{children}</SidebarView>
<Footer>
<ZoomInOutlined />
<ZoomSlider defaultValue={size} max={256} min={64} onChange={setSize} step={8} />
</Footer>
</View>
</DraggablePanel>
</>
);
});
export default Sidebar;

View File

@ -1,28 +0,0 @@
import {memo, useEffect} from 'react';
interface GiscusProps {
themeMode: 'light' | 'dark';
}
const Giscus = memo<GiscusProps>(({themeMode}) => {
useEffect(() => {
// giscus
const giscus: HTMLScriptElement = document.createElement('script');
giscus.src = 'https://giscus.app/client.js';
giscus.dataset.repo = 'canisminor1990/sd-webui-kitchen-theme';
giscus.dataset.repoId = 'R_kgDOJCPcNg';
giscus.dataset.mapping = 'number';
giscus.dataset.term = '53';
giscus.dataset.reactionsEnabled = '1';
giscus.dataset.emitMetadata = '0';
giscus.dataset.inputPosition = 'bottom';
giscus.dataset.theme = themeMode;
giscus.dataset.lang = 'en';
giscus.crossOrigin = 'anonymous';
giscus.async = true;
document.querySelectorAll('head')[0].append(giscus);
}, []);
return <div className="giscus" id="giscus" />;
});
export default Giscus;

View File

@ -1,33 +0,0 @@
import {TabsNav, type TabsNavProps} from '@lobehub/ui';
import {memo, useEffect, useState} from 'react';
const getNavButtons: HTMLButtonElement[] | any = () =>
gradioApp().querySelectorAll('#tabs > .tab-nav:first-child button') || [];
const onChange: TabsNavProps['onChange'] = (activeKey) => {
const buttons = getNavButtons();
buttons[Number(activeKey)]?.click();
};
const Nav = memo(() => {
const [items, setItems] = useState<TabsNavProps['items']>([]);
useEffect(() => {
onUiLoaded(() => {
const buttons = getNavButtons();
const list: TabsNavProps['items'] | any = [];
buttons.forEach((button: HTMLButtonElement | any, index: number) => {
button.id = `kitchen-nav-${index}`;
list.push({
key: String(index),
label: button.textContent,
});
});
setItems(list.filter(Boolean));
});
}, []);
return <TabsNav items={items} onChange={onChange} />;
});
export default Nav;

View File

@ -1,123 +0,0 @@
import {SettingOutlined} from '@ant-design/icons';
import {Button, Divider, Form, InputNumber, Popover, Segmented, Space, Switch} from 'antd';
import {memo, useCallback} from 'react';
import styled from 'styled-components';
import {shallow} from 'zustand/shallow';
import {WebuiSetting, defaultSetting, useAppStore} from '@/store';
/******************************************************
*********************** Style *************************
******************************************************/
const FormItem = styled(Form.Item)`
margin-bottom: 8px;
.ant-row {
justify-content: space-between;
> div {
flex: unset !important;
flex-grow: unset !important;
}
}
`;
const Title = styled.div`
font-size: 16px;
font-weight: 600;
`;
const SubTitle = styled.div`
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
`;
/******************************************************
************************* Dom *************************
******************************************************/
const Setting = memo(() => {
const [setting, onSetSetting] = useAppStore((st) => [st.setting, st.onSetSetting], shallow);
const onReset = useCallback(() => {
onSetSetting(defaultSetting);
gradioApp().querySelector('#settings_restart_gradio')?.click();
}, []);
const onFinish = useCallback((value: WebuiSetting) => {
onSetSetting(value);
gradioApp().querySelector('#settings_restart_gradio')?.click();
}, []);
return (
<Popover
content={
<Form
initialValues={setting}
layout="horizontal"
onFinish={onFinish}
size="small"
style={{maxWidth: 260}}
>
<Divider style={{margin: '4px 0 8px'}} />
<SubTitle>Promot Textarea</SubTitle>
<FormItem label="Display mode" name="promotTextarea">
<Segmented options={['scroll', 'resizable']} />
</FormItem>
<Divider style={{margin: '4px 0 8px'}} />
<SubTitle>Sidebar</SubTitle>
<FormItem label="Default expand" name="sidebarExpand" valuePropName="checked">
<Switch />
</FormItem>
<FormItem label="Display mode" name="sidebarFixedMode">
<Segmented options={['fixed', 'float']} />
</FormItem>
<FormItem label="Default width" name="sidebarWidth">
<InputNumber />
</FormItem>
<Divider style={{margin: '4px 0 8px'}} />
<SubTitle>ExtraNetwork Sidebar</SubTitle>
<FormItem label="Enable" name="enableExtraNetworkSidebar" valuePropName="checked">
<Switch />
</FormItem>
<FormItem label="Display mode" name="extraNetworkFixedMode" valuePropName="checked">
<Segmented options={['fixed', 'float']} />
</FormItem>
<FormItem label="Default expand" name="extraNetworkSidebarExpand" valuePropName="checked">
<Switch />
</FormItem>
<FormItem label="Default width" name="extraNetworkSidebarWidth">
<InputNumber />
</FormItem>
<FormItem label="Default card size" name="extraNetworkCardSize">
<InputNumber />
</FormItem>
<Divider style={{margin: '4px 0 8px'}} />
<SubTitle>Other</SubTitle>
<FormItem label="Use svg icons" name="svgIcon" valuePropName="checked">
<Switch />
</FormItem>
<Divider style={{margin: '4px 0 16px'}} />
<FormItem>
<Space>
<Button htmlType="button" onClick={onReset} style={{borderRadius: 4}}>
Reset
</Button>
<Button htmlType="submit" style={{borderRadius: 4}} type="primary">
Apply and restart UI
</Button>
</Space>
</FormItem>
</Form>
}
title={<Title> Setting</Title>}
trigger="click"
>
<Button icon={<SettingOutlined />} title="Setting" />
</Popover>
);
});
export default Setting;

View File

@ -1,94 +0,0 @@
import {BoldOutlined, GithubOutlined} from '@ant-design/icons';
import {Header as H} from '@lobehub/ui';
import {Button, Modal, Space} from 'antd';
import qs from 'query-string';
import {type ReactNode, memo, useCallback, useState} from 'react';
import {shallow} from 'zustand/shallow';
import {useAppStore} from '@/store';
import Giscus from './Giscus';
import Logo from './Logo';
import Nav from './Nav';
import Setting from './Setting';
import {civitaiLogo, themeIcon} from './style';
interface HeaderProps {
children: ReactNode;
}
const Header = memo<HeaderProps>(({children}) => {
const [themeMode] = useAppStore((st) => [st.themeMode], shallow);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleSetTheme = useCallback(() => {
const theme = themeMode === 'light' ? 'dark' : 'light';
const gradioURL = qs.parseUrl(window.location.href);
gradioURL.query.__theme = theme;
window.location.replace(qs.stringifyUrl(gradioURL));
}, [themeMode]);
const showModal = () => setIsModalOpen(true);
const handleCancel = () => setIsModalOpen(false);
return (
<>
<H
actions={
<Space.Compact>
<a href="https://civitai.com/" rel="noreferrer" target="_blank">
<Button icon={civitaiLogo} title="Civitai" />
</a>
<a
href="https://www.birme.net/?target_width=512&target_height=512"
rel="noreferrer"
target="_blank"
>
<Button icon={<BoldOutlined />} title="Birme" />
</a>
<Button icon={<GithubOutlined />} onClick={showModal} title="Feedback" />
<Setting />
<Button icon={themeIcon[themeMode]} onClick={handleSetTheme} title="Switch Theme" />
</Space.Compact>
}
logo={
<a
href="https://github.com/canisminor1990/sd-webui-kitchen-theme"
rel="noreferrer"
target="_blank"
>
<Logo themeMode={themeMode} />
</a>
}
nav={
<>
<Nav />
{children}
</>
}
/>
<Modal
footer={false}
onCancel={handleCancel}
open={isModalOpen}
title={
<a
href="https://github.com/canisminor1990/sd-webui-kitchen-theme"
rel="noreferrer"
target="_blank"
>
<Space>
<GithubOutlined />
{'canisminor1990/sd-webui-kitchen-theme'}
</Space>
</a>
}
>
<Giscus themeMode={themeMode} />
</Modal>
</>
);
});
export default Header;

View File

@ -1,29 +0,0 @@
export const themeIcon = {
dark: (
<span className="anticon anticon-github" role="img">
<svg fill="currentColor" height="1em" viewBox="0 0 16 16" width="1em">
<path d="M8.218 1.455c3.527.109 6.327 3.018 6.327 6.545 0 3.6-2.945 6.545-6.545 6.545a6.562 6.562 0 0 1-6.036-4h.218c3.6 0 6.545-2.945 6.545-6.545 0-.91-.182-1.745-.509-2.545m0-1.455c-.473 0-.909.218-1.2.618-.29.4-.327.946-.145 1.382.254.655.4 1.31.4 2 0 2.8-2.291 5.09-5.091 5.09h-.218c-.473 0-.91.22-1.2.62-.291.4-.328.945-.146 1.38C1.891 14.074 4.764 16 8 16c4.4 0 8-3.6 8-8a7.972 7.972 0 0 0-7.745-8h-.037Z"></path>
</svg>
</span>
),
light: (
<span className="anticon anticon-github" role="img">
<svg fill="currentColor" height="1em" viewBox="0 0 16 16" width="1em">
<path d="M8 13a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1ZM8 3a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v1a1 1 0 0 1-1 1Zm7 4a1 1 0 1 1 0 2h-1a1 1 0 1 1 0-2h1ZM3 8a1 1 0 0 1-1 1H1a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1Zm9.95 3.536.707.707a1 1 0 0 1-1.414 1.414l-.707-.707a1 1 0 0 1 1.414-1.414Zm-9.9-7.072-.707-.707a1 1 0 0 1 1.414-1.414l.707.707A1 1 0 0 1 3.05 4.464Zm9.9 0a1 1 0 0 1-1.414-1.414l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707Zm-9.9 7.072a1 1 0 0 1 1.414 1.414l-.707.707a1 1 0 0 1-1.414-1.414l.707-.707ZM8 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0 6.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"></path>
</svg>
</span>
),
};
export const darkLogo =
'https://gw.alipayobjects.com/zos/bmw-prod/9ecb2822-1592-4cb0-a087-ce0097fef2ca.svg';
export const lightLogo =
'https://gw.alipayobjects.com/zos/bmw-prod/e146116d-c65a-4306-a3d2-bb8d05e1c49b.svg';
export const civitaiLogo = (
<span className="anticon civitai" role="img">
<svg fill="currentColor" height="1em" viewBox="0 0 16 16" width="1em">
<path d="M2 4.5L8 1l6 3.5v7L8 15l-6-3.5v-7zm6-1.194L3.976 5.653v4.694L8 12.694l4.024-2.347V5.653L8 3.306zm0 1.589l2.662 1.552v.824H9.25L8 6.54l-1.25.73v1.458L8 9.46l1.25-.73h1.412v.824L8 11.105 5.338 9.553V6.447L8 4.895z" />
</svg>
</span>
);

View File

@ -1,101 +0,0 @@
import {memo, useCallback, useState} from 'react';
import styled from 'styled-components';
import TagList, {PromptType, TagItem} from './TagList';
import {formatPrompt} from './utils';
/******************************************************
*********************** Style *************************
******************************************************/
const View = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const ButtonGroup = styled.div`
display: flex;
gap: 8px;
`;
const Button = styled.button`
cursor: pointer;
position: relative;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding: var(--input-padding);
font-size: var(--input-text-size);
font-weight: var(--input-text-weight);
line-height: var(--line-sm);
background: var(--button-secondary-background-fill);
border: var(--button-border-width) solid var(--button-secondary-border-color);
border-radius: var(--input-radius);
`;
/******************************************************
************************* Dom *************************
******************************************************/
interface PromptProps {
type: PromptType;
}
const Prompt = memo<PromptProps>(({type}) => {
const [tags, setTags] = useState<TagItem[]>([]);
const id =
type === 'positive' ? "[id$='2img_prompt'] textarea" : "[id$='2img_neg_prompt'] textarea";
const getValue = useCallback(() => {
try {
const textarea: HTMLTextAreaElement | any = get_uiCurrentTabContent().querySelector(id);
if (textarea) setTags(formatPrompt(textarea.value));
} catch (error) {
console.log(error);
}
}, []);
const setValue = useCallback(() => {
try {
const textarea: HTMLTextAreaElement | any = get_uiCurrentTabContent().querySelector(id);
if (textarea) textarea.value = tags.map((t) => t.text).join(', ');
updateInput(textarea);
} catch (error) {
console.log(error);
}
}, [tags]);
const setCurrentValue = useCallback((currentTags: TagItem[]) => {
try {
const textarea: HTMLTextAreaElement | any = get_uiCurrentTabContent().querySelector(id);
if (textarea) textarea.value = currentTags.map((t) => t.text).join(', ');
updateInput(textarea);
} catch (error) {
console.log(error);
}
}, []);
return (
<View>
<TagList setTags={setTags} setValue={setCurrentValue} tags={tags} type={type} />
<ButtonGroup>
<Button onClick={getValue} title="Load Prompt">
🔄
</Button>
<Button onClick={setValue} title="Set Prompt">
</Button>
</ButtonGroup>
</View>
);
});
export default Prompt;

View File

@ -1,65 +0,0 @@
import {DraggablePanel} from '@lobehub/ui';
import {useResponsive} from 'antd-style';
import {type CSSProperties, type ReactNode, memo, useEffect, useState} from 'react';
import styled from 'styled-components';
import {shallow} from 'zustand/shallow';
import {useAppStore} from '@/store';
import PromptGroup from './PromptGroup';
/******************************************************
*********************** Style *************************
******************************************************/
const SidebarView = styled.div`
overflow-x: hidden;
overflow-y: auto;
height: var(--fill-available);
padding: 16px;
`;
/******************************************************
************************* Dom *************************
******************************************************/
interface SidebarProps {
children: ReactNode;
loading?: boolean;
style?: CSSProperties;
}
const Sidebar = memo<SidebarProps>(({children, loading, style}) => {
const [setting] = useAppStore((st) => [st.setting], shallow);
const [mode] = useState<'fixed' | 'float'>(setting.sidebarFixedMode);
const {mobile} = useResponsive();
const [expand, setExpand] = useState<boolean>(setting.sidebarExpand);
useEffect(() => {
if (mobile) setExpand(false);
}, []);
return (
<DraggablePanel
defaultSize={{width: setting.sidebarWidth}}
expand={expand}
minWidth={setting.sidebarWidth}
mode={mode}
onExpandChange={setExpand}
pin={mode === 'fixed'}
placement="left"
style={{
display: 'flex',
flexDirection: 'column',
...style,
}}
>
<SidebarView>
{!loading && <PromptGroup />}
{children}
</SidebarView>
</DraggablePanel>
);
});
export default Sidebar;

View File

@ -1,48 +0,0 @@
import negativeData from '@/data/negative.json';
import positiveData from '@/data/positive.json';
import {Converter} from '@/script/formatPrompt';
import {TagItem} from './TagList';
export const genTagType = (tag: TagItem): TagItem => {
const newTag = tag;
if (newTag.text.includes('<lora')) {
newTag.className = 'ReactTags__lora';
} else if (newTag.text.includes('<hypernet')) {
newTag.className = 'ReactTags__hypernet';
} else if (newTag.text.includes('<embedding')) {
newTag.className = 'ReactTags__embedding';
} else {
newTag.className = undefined;
}
return newTag;
};
export const formatPrompt = (value: string) => {
const text = Converter.convertStr(value);
const textArray = Converter.convertStr2Array(text).map((item) => {
if (item.includes('<')) return item;
const newItem = item
.replaceAll(/\s+/g, ' ')
.replaceAll(/|\.\|。/g, ',')
.replaceAll(/“||”|"|\/'/g, '')
.replaceAll(', ', ',')
.replaceAll(',,', ',')
.replaceAll(',', ', ');
return Converter.convertStr2Array(newItem).join(', ');
});
return textArray.map((tag) => genTagType({id: tag, text: tag}));
};
const genSuggestions = (array: string[]) =>
array.map((text) => {
return {
id: text,
text,
};
});
export const suggestions = {
negative: genSuggestions(negativeData),
positive: genSuggestions(positiveData),
};

View File

@ -1,5 +0,0 @@
export { default as Content } from './Content';
export { default as DraggablePanel } from './DraggablePanel';
export { default as ExtraNetworkSidebar } from './ExtraNetworkSidebar';
export { default as Header } from './Header';
export { default as Sidebar } from './Sidebar';

View File

@ -1,29 +0,0 @@
import {useEffect, useState} from 'react';
function checkIsDarkMode() {
try {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
} catch {
return false;
}
}
export function useIsDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(checkIsDarkMode());
useEffect(() => {
const mqList = window.matchMedia('(prefers-color-scheme: dark)');
const listener = (event: any) => {
setIsDarkMode(event.matches);
};
mqList.addEventListener('change', listener);
return () => {
mqList.removeEventListener('change', listener);
};
}, []);
return isDarkMode;
}

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from 'react';
function checkIsDarkMode() {
try {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
} catch {
return false;
}
}
export function useIsDarkMode() {
const [isDarkMode, setIsDarkMode] = useState(checkIsDarkMode());
useEffect(() => {
const mqList = window.matchMedia('(prefers-color-scheme: dark)');
const listener = (event: any) => {
setIsDarkMode(event.matches);
};
mqList.addEventListener('change', listener);
return () => {
mqList.removeEventListener('change', listener);
};
}, []);
return isDarkMode;
}

298
src/layouts/GlobalStyle.ts Normal file
View File

@ -0,0 +1,298 @@
import { createGlobalStyle, css } from 'antd-style';
import { readableColor } from 'polished';
const GlobalStyle = createGlobalStyle`
${({ theme }) => {
const checkBoxIcon = `data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='${encodeURIComponent(
theme.colorBgLayout,
)}' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e`;
const radioIcon = `data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='${encodeURIComponent(
theme.colorBgLayout,
)}' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e`;
return css`
:root,
.dark {
--primary-50: ${theme.geekblue1};
--primary-100: ${theme.geekblue2};
--primary-200: ${theme.geekblue3};
--primary-300: ${theme.geekblue4};
--primary-400: ${theme.geekblue5};
--primary-500: ${theme.geekblue6};
--primary-600: ${theme.geekblue7};
--primary-700: ${theme.geekblue8};
--primary-800: ${theme.geekblue9};
--primary-900: ${theme.geekblue10};
--primary-950: ${theme.geekblue11};
--secondary-50: ${theme.geekblue1};
--secondary-100: ${theme.geekblue2};
--secondary-200: ${theme.geekblue3};
--secondary-300: ${theme.geekblue4};
--secondary-400: ${theme.geekblue5};
--secondary-500: ${theme.geekblue6};
--secondary-600: ${theme.geekblue7};
--secondary-700: ${theme.geekblue8};
--secondary-800: ${theme.geekblue9};
--secondary-900: ${theme.geekblue10};
--secondary-950: ${theme.geekblue11};
--neutral-50: ${theme.colorText};
--neutral-100: ${theme.colorText};
--neutral-200: ${theme.colorTextSecondary};
--neutral-300: ${theme.colorTextTertiary};
--neutral-400: ${theme.colorTextQuaternary};
--neutral-500: ${theme.colorFill};
--neutral-600: ${theme.colorFillSecondary};
--neutral-700: ${theme.colorFillTertiary};
--neutral-800: ${theme.colorFillQuaternary};
--neutral-900: ${theme.colorBgElevated};
--neutral-950: ${theme.colorBgContainer};
--spacing-xxs: ${theme.paddingXXS / 4}px;
--spacing-xs: ${theme.paddingXS / 4}px;
--spacing-sm: ${theme.paddingSM / 4}px;
--spacing-md: ${theme.paddingMD / 4}px;
--spacing-lg: ${theme.paddingLG / 4}px;
--spacing-xl: ${theme.paddingXL / 4}px;
--spacing-xxl: ${theme.paddingXL / 4}px;
--radius-xxs: ${theme.borderRadiusXS}px;
--radius-xs: ${theme.borderRadiusXS}px;
--radius-sm: ${theme.borderRadiusSM}px;
--radius-md: ${theme.borderRadius}px;
--radius-lg: ${theme.borderRadius}px;
--radius-xl: ${theme.borderRadiusLG}px;
--radius-xxl: ${theme.borderRadiusLG}px;
--text-xxs: ${theme.fontSizeSM}px;
--text-xs: ${theme.fontSizeSM}px;
--text-sm: ${theme.fontSize}px;
--text-md: ${theme.fontSize}px;
--text-lg: ${theme.fontSizeLG}px;
--text-xl: ${theme.fontSizeXL}px;
--text-xxl: ${theme.fontSizeXL}px;
--font: ${theme.fontFamily};
--font-mono: ${theme.fontFamilyCode};
--body-background-fill: ${theme.colorBgLayout};
--body-text-color: ${theme.colorText};
--body-text-size: ${theme.fontSize}px;
--body-text-weight: 400;
--embed-radius: var(--radius-lg);
--color-accent: ${theme.colorPrimary};
--color-accent-soft: ${theme.colorPrimaryHover};
--background-fill-primary: ${theme.colorBgLayout};
--background-fill-secondary: var(--neutral-50);
--border-color-accent: ${theme.colorBorder};
--border-color-primary: ${theme.colorBorderSecondary};
--link-text-color: ${theme.colorInfoText};
--link-text-color-active: ${theme.colorInfoTextActive};
--link-text-color-hover: ${theme.colorInfoTextHover};
--link-text-color-visited: ${theme.colorInfoText};
--body-text-color-subdued: ${theme.colorTextDescription};
--shadow-drop: ${theme.boxShadowSecondary};
--shadow-drop-lg: ${theme.boxShadow};
--shadow-inset: ${theme.boxShadowSecondary} inset;
--shadow-spread: 3px;
--block-background-fill: ${theme.colorBgContainer};
--block-border-color: ${theme.colorBorderSecondary};
--block-border-width: 1px;
--block-info-text-color: ${theme.colorTextSecondary};
--block-info-text-size: var(--text-sm);
--block-info-text-weight: 400;
--block-label-background-fill: ${theme.colorFillSecondary};
--block-label-border-color: ${theme.colorBorderSecondary};
--block-label-border-width: 1px;
--block-label-shadow: ${theme.boxShadowTertiary};
--block-label-text-color: ${theme.colorText}
--block-label-margin: 0;
--block-label-padding: var(--spacing-sm) var(--spacing-lg);
--block-label-radius: ${theme.borderRadius}px;
--block-label-right-radius: ${theme.borderRadius}px;
--block-label-text-size: var(--text-sm);
--block-label-text-weight: 400;
--block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px);
--block-radius: var(--radius-lg);
--block-shadow: ${theme.boxShadowSecondary};
--block-title-background-fill: none;
--block-title-border-color: none;
--block-title-border-width: 0;
--block-title-text-color: ${theme.colorTextDescription};
--block-title-padding: 0;
--block-title-radius: none;
--block-title-text-size: var(--text-md);
--block-title-text-weight: 400;
--container-radius: var(--radius-lg);
--form-gap-width: 1px;
--layout-gap: var(--spacing-xxl);
--panel-background-fill: ${theme.colorBgContainer};
--panel-border-color: ${theme.colorBorderSecondary};
--panel-border-width: 0;
--section-header-text-size: var(--text-md);
--section-header-text-weight: 400;
--chatbot-code-background-color: ${theme.colorBgContainer};
--checkbox-background-color: ${theme.colorFillTertiary};
--checkbox-background-color-focus: ${theme.colorFillSecondary};
--checkbox-background-color-hover: ${theme.colorFillSecondary};
--checkbox-background-color-selected: ${theme.colorText};
--checkbox-border-color: ${theme.colorBorderSecondary};
--checkbox-border-color-focus: ${theme.colorBorder};
--checkbox-border-color-hover: ${theme.colorBorder};
--checkbox-border-color-selected: ${theme.colorText};
--checkbox-border-radius: ${theme.borderRadiusXS}px;
--checkbox-border-width: 1px;
--checkbox-label-background-fill: linear-gradient(
to top,
${theme.colorFillTertiary},
${theme.colorBgLayout}
);
--checkbox-label-background-fill-hover: linear-gradient(
to top,
${theme.colorFillSecondary},
${theme.colorBgLayout}
);
--checkbox-label-background-fill-selected: ${theme.colorFillSecondary};
--checkbox-label-border-color: ${theme.colorBorderSecondary};
--checkbox-label-border-color-hover: ${theme.colorBorder};
--checkbox-label-border-width: 1px;
--checkbox-label-gap: var(--spacing-lg);
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
--checkbox-label-shadow: none;
--checkbox-label-text-size: var(--text-md);
--checkbox-label-text-weight: 400;
--checkbox-check: url(${checkBoxIcon});
--radio-circle: url(${radioIcon});
--checkbox-shadow: none;
--checkbox-label-text-color: ${theme.colorTextDescription};
--checkbox-label-text-color-selected: ${theme.colorText};
--error-background-fill: linear-gradient(
to right,
${theme.colorErrorBg},
${theme.colorFillSecondary}
);
--error-border-color: ${theme.colorErrorBorder};
--error-border-width: 1px;
--error-text-color: ${theme.colorErrorText};
--input-background-fill: ${theme.colorFillTertiary};
--input-background-fill-focus: ${theme.colorFillSecondary};
--input-background-fill-hover: ${theme.colorFillSecondary};
--input-border-color: ${theme.colorBorderSecondary};
--input-border-color-focus: ${theme.colorBorder};
--input-border-color-hover: ${theme.colorBorder};
--input-border-width: 1px;
--input-padding: var(--spacing-xl);
--input-placeholder-color: ${theme.colorTextQuaternary};
--input-radius: var(--radius-lg);
--input-shadow: none;
--input-shadow-focus: none;
--input-text-size: var(--text-md);
--input-text-weight: 400;
--loader-color: ${theme.colorFillSecondary};
--prose-text-size: var(--text-md);
--prose-text-weight: 400;
--prose-header-text-weight: 600;
--slider-color: ${theme.colorPrimary};
--stat-background-fill: linear-gradient(to right,
${theme.colorInfo},
${theme.colorInfoHover});
--table-border-color: ${theme.colorBorderSecondary};
--table-even-background-fill: transparent;
--table-odd-background-fill: ${theme.colorFillTertiary};
--table-radius: var(--radius-lg);
--table-row-focus: ${theme.colorFillSecondary};
--button-border-width: 1px;
--button-cancel-background-fill: ${theme.colorError};
--button-cancel-background-fill-hover: ${theme.colorErrorHover};
--button-cancel-border-color: ${theme.colorErrorBorder};
--button-cancel-border-color-hover: ${theme.colorErrorBorderHover};
--button-cancel-text-color: ${readableColor(theme.colorError)};
--button-cancel-text-color-hover: ${readableColor(theme.colorError)};
--button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg));
--button-large-radius: var(--radius-lg);
--button-large-text-size: var(--text-lg);
--button-large-text-weight: 600;
--button-primary-background-fill: ${theme.colorPrimary};
--button-primary-background-fill-hover: ${theme.colorPrimaryHover};
--button-primary-border-color: ${theme.colorPrimaryBorder};
--button-primary-border-color-hover: ${theme.colorPrimaryBorderHover};
--button-primary-text-color: ${readableColor(theme.colorPrimary)};
--button-primary-text-color-hover: ${readableColor(theme.colorPrimary)};
--button-secondary-background-fill: ${theme.colorFillSecondary};
--button-secondary-background-fill-hover: ${theme.colorFill};;
--button-secondary-border-color: ${theme.colorBorderSecondary};
--button-secondary-border-color-hover: ${theme.colorBorder};
--button-secondary-text-color: ${theme.colorTextSecondary};
--button-secondary-text-color-hover: ${theme.colorText};
--button-shadow: none;
--button-shadow-active: none;
--button-shadow-hover: none;
--button-small-padding: var(--spacing-sm) calc(2 * var(--spacing-sm));
--button-small-radius: var(--radius-lg);
--button-small-text-size: var(--text-md);
--button-small-text-weight: 400;
--button-transition: none;
}
.ant-tooltip-inner {
display: flex;
align-items: center;
justify-content: center;
min-height: unset;
padding: 4px 8px;
color: ${theme.colorBgLayout};
background-color: ${theme.colorText} !important;
border-radius: ${theme.borderRadiusSM}px;
}
.ant-tooltip-arrow {
&::before,
&::after {
background: ${theme.colorText} !important;
}
}
button {
cursor: pointer;
background: ${theme.colorFillSecondary};
&:hover {
background: ${theme.colorFill};
}
}
input[type="range"]{
cursor: pointer;
height: 3px;
margin-top: 8px;
appearance: none;
background: ${theme.colorBorder};
border-radius: ${theme.borderRadiusXS}px;
outline: none;
&::-webkit-slider-thumb {
width: 12px;
height:16px;
appearance: none;
background: ${theme.colorBgElevated};
border: 1px solid ${theme.colorBorder};
border-radius: ${theme.borderRadiusSM}px;
&:hover,&:active {
background: ${theme.colorText};
border-color: ${theme.colorText};
}
}
&:hover {
background: ${theme.colorTextDescription};
}
&:active {
background: ${theme.colorPrimary};
}
}
`;
}}
`;
export default GlobalStyle;

53
src/layouts/index.tsx Normal file
View File

@ -0,0 +1,53 @@
import { DivProps, ThemeProvider } from '@lobehub/ui';
import qs from 'query-string';
import { memo, useEffect, useState } from 'react';
import { shallow } from 'zustand/shallow';
import { useIsDarkMode } from '@/hooks/useIsDarkMode';
import { useAppStore } from '@/store';
import GlobalStyle from './GlobalStyle';
import { useStyles } from './style';
const Layout = memo<DivProps>(({ children }) => {
const { onSetThemeMode, onInit } = useAppStore(
(st) => ({ onInit: st.onInit, onSetThemeMode: st.onSetThemeMode }),
shallow,
);
const isDarkMode = useIsDarkMode();
const [appearance, setAppearance] = useState<'light' | 'dark'>('light');
const [first, setFirst] = useState(true);
const { styles } = useStyles();
useEffect(() => {
onInit();
}, []);
useEffect(() => {
const queryTheme: any = String(qs.parseUrl(window.location.href).query.__theme || '');
if (queryTheme) {
setAppearance(queryTheme as any);
document.body.classList.add(queryTheme);
onSetThemeMode(queryTheme);
return;
}
setAppearance(isDarkMode ? 'dark' : 'light');
document.body.classList.add(isDarkMode ? 'dark' : 'light');
onSetThemeMode(isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
useEffect(() => {
if (first) {
setFirst(false);
return;
}
window.location.reload();
}, [isDarkMode]);
return (
<ThemeProvider themeMode={appearance}>
<GlobalStyle />
<div className={styles}>{children}</div>
</ThemeProvider>
);
});
export default Layout;

6
src/layouts/style.ts Normal file
View File

@ -0,0 +1,6 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css, token }) => {
console.log(token.colorText);
return css``;
});

25
src/main.tsx Normal file
View File

@ -0,0 +1,25 @@
import { createRoot } from 'react-dom/client';
import App from './App';
if (window.global === undefined) window.global = window;
if (window.location.href.includes('dev') && import.meta.env.VITE_CONTEXT !== 'DEV') {
console.log('🚧 Theme Loader in Dev Mode');
} else {
document.addEventListener(
'DOMContentLoaded',
() => {
const root = document.createElement('div');
root.setAttribute('id', 'root');
try {
gradioApp()?.append(root);
} catch {
document.querySelector('gradio-app')?.append(root);
}
const client = createRoot(root);
client.render(<App />);
},
{ once: true },
);
}

View File

@ -0,0 +1,27 @@
import { useResponsive } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useRef } from 'react';
import dragablePanel from '@/script/draggablePanel';
import { useAppStore } from '@/store';
import { DivProps } from '@/types';
import { useStyles } from './style';
const Content = memo<DivProps>(({ className, ...props }) => {
const mainReference: any = useRef<HTMLElement>();
const setting = useAppStore((st) => st.setting, isEqual);
const { cx, styles } = useStyles({ isPromptResizable: setting.promotTextarea === 'resizable' });
const { mobile } = useResponsive();
useEffect(() => {
// Content
const main = gradioApp().querySelector('.app');
if (main) mainReference.current?.append(main);
if (!mobile) dragablePanel();
}, []);
return <div className={cx(styles.container, className)} ref={mainReference} {...props} />;
});
export default Content;

View File

@ -0,0 +1,19 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(
({ css }, { isPromptResizable }: { isPromptResizable: boolean }) => ({
container: css`
position: relative;
flex: 1;
max-width: 100%;
[id$='2img_prompt'] textarea {
max-height: ${isPromptResizable ? 'unset' : '84px'};
}
[id$='2img_neg_prompt'] textarea {
max-height: ${isPromptResizable ? 'unset' : '84px'};
}
`,
}),
);

View File

@ -0,0 +1,86 @@
import { Icon } from '@lobehub/ui';
import { Skeleton, Slider } from 'antd';
import { ZoomIn } from 'lucide-react';
import { memo, useEffect, useRef, useState } from 'react';
import { shallow } from 'zustand/shallow';
import { useStyles } from '@/pages/ExtraNetworkSidebar/style';
import civitaiHelperFix from '@/script/civitaiHelperFix';
import { useAppStore } from '@/store';
const Inner = memo(() => {
const txt2imgExtraNetworkSidebarReference: any = useRef<HTMLElement>();
const img2imgExtraNetworkSidebarReference: any = useRef<HTMLElement>();
const [extraLoading, setExtraLoading] = useState(true);
const [setting, currentTab] = useAppStore((st) => [st.setting, st.currentTab], shallow);
const [size, setSize] = useState<number>(setting?.extraNetworkCardSize || 86);
const { styles } = useStyles({ size });
useEffect(() => {
if (setting.enableExtraNetworkSidebar) {
const txt2imgExtraNetworks = gradioApp().querySelector('div#txt2img_extra_networks');
const img2imgExtraNetworks = gradioApp().querySelector('div#img2img_extra_networks');
if (txt2imgExtraNetworks && img2imgExtraNetworks) {
txt2imgExtraNetworkSidebarReference.current?.append(txt2imgExtraNetworks);
img2imgExtraNetworkSidebarReference.current?.append(img2imgExtraNetworks);
}
if (document.querySelector('#txt2img_lora_cards')) {
civitaiHelperFix();
setExtraLoading(false);
return;
}
setTimeout(() => {
const t2indexButton: any = document.querySelector('#txt2img_extra_refresh');
const index2indexButton: any = document.querySelector('#img2img_extra_refresh');
t2indexButton.click();
index2indexButton.click();
setExtraLoading(false);
try {
const civitaiButton = document.querySelectorAll(
'button[title="Refresh Civitai Helper\'s additional buttons"]',
) as NodeListOf<HTMLButtonElement>;
if (civitaiButton) {
for (const button of civitaiButton) {
button.onclick = civitaiHelperFix;
}
}
civitaiHelperFix();
} catch (error) {
console.log(error);
}
}, 2000);
}
}, []);
return (
<>
<div className={styles.list}>
{extraLoading && <Skeleton active />}
<div style={extraLoading ? { display: 'none' } : {}}>
<div
id="txt2img-extra-netwrok-sidebar"
ref={txt2imgExtraNetworkSidebarReference}
style={currentTab === 'tab_img2img' ? { display: 'none' } : {}}
/>
<div
id="img2img-extra-netwrok-sidebar"
ref={img2imgExtraNetworkSidebarReference}
style={currentTab === 'tab_img2img' ? {} : { display: 'none' }}
/>
</div>
</div>
<div className={styles.footer}>
<Icon icon={ZoomIn} />
<Slider
defaultValue={size}
max={256}
min={64}
onChange={setSize}
step={8}
style={{ flex: 1 }}
/>
</div>
</>
);
});
export default Inner;

View File

@ -0,0 +1,46 @@
import { DraggablePanel, LayoutSidebarInner } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useState } from 'react';
import { useAppStore } from '@/store';
import { DivProps } from '@/types/index';
import Inner from './Inner';
import { useStyles } from './style';
export interface ExtraNetworkSidebarProps extends DivProps {
headerHeight: number;
}
const ExtraNetworkSidebar = memo<ExtraNetworkSidebarProps>(({ headerHeight }) => {
const setting = useAppStore((st) => st.setting, isEqual);
const [mode] = useState<'fixed' | 'float'>(setting.extraNetworkFixedMode);
const [expand, setExpand] = useState<boolean>(setting.extraNetworkSidebarExpand);
const { mobile } = useResponsive();
const { styles } = useStyles({ headerHeight });
useEffect(() => {
if (mobile) setExpand(false);
}, []);
return (
<DraggablePanel
defaultSize={{ width: setting.extraNetworkSidebarWidth }}
expand={expand}
minWidth={setting.extraNetworkSidebarWidth}
mode={mode}
onExpandChange={setExpand}
pin={mode === 'fixed'}
placement="right"
>
<LayoutSidebarInner>
<div className={styles.container}>
<Inner />
</div>
</LayoutSidebarInner>
</DraggablePanel>
);
});
export default ExtraNetworkSidebar;

View File

@ -0,0 +1,66 @@
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
export const useStyles = createStyles(
(
{ css, cx, stylish, token },
{ headerHeight = 64, size = 86 }: { headerHeight?: number; size?: number },
) => ({
container: cx(
stylish.blur,
css`
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
height: calc(100vh - ${headerHeight}px);
background: ${rgba(token.colorBgLayout, 0.5)};
button#txt2img_extra_networks,
button#img2img_extra_networks {
display: none !important;
}
`,
),
footer: css`
display: flex;
flex: 0;
gap: 8px;
align-items: center;
justify-content: flex-start;
padding: 8px 16px;
border-top: 1px solid ${token.colorBorderSecondary};
`,
list: css`
overflow-x: hidden;
overflow-y: auto;
flex: 1;
padding: 16px;
#txt2img_extra_networks,
#img2img_extra_networks {
display: block !important;
}
.extra-network-cards .actions .name {
background: unset !important;
}
.extra-network-cards,
.extra-network-thumbs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(${size}px, 1fr));
> .card {
width: var(--fill-available) !important;
height: ${size * 1.5}px !important;
}
}
`,
}),
);

82
src/pages/Footer/data.tsx Normal file
View File

@ -0,0 +1,82 @@
import { Icon } from '@lobehub/ui';
import { FooterProps as FProps } from '@lobehub/ui/es/Footer';
import { Bug, FileClock, GitFork, Github } from 'lucide-react';
import { homepage } from '@/../package.json';
export const columns: FProps['columns'] = [
{
items: [
{
description: 'AIGC Components',
openExternal: true,
title: '🤯 Lobe UI',
url: 'https://github.com/lobehub/lobe-ui',
},
{
description: 'Chatbot Client',
openExternal: true,
title: '🤯 Lobe Chat',
url: 'https://github.com/lobehub/lobe-chat',
},
{
description: 'Node Flow Editor',
openExternal: true,
title: '🤯 Lobe Flow',
url: 'https://github.com/lobehub/lobe-flow',
},
],
title: 'Resources',
},
{
items: [
{
icon: <Icon icon={Bug} size="small" />,
openExternal: true,
title: 'Report Bug',
url: `${homepage}/issues/new/choose`,
},
{
icon: <Icon icon={GitFork} size="small" />,
openExternal: true,
title: 'Request Feature',
url: `${homepage}/issues/new/choose`,
},
],
title: 'Community',
},
{
items: [
{
icon: <Icon icon={Github} size="small" />,
openExternal: true,
title: 'GitHub',
url: homepage,
},
{
icon: <Icon icon={FileClock} size="small" />,
openExternal: true,
title: 'Changelog',
url: `${homepage}/blob/main/CHANGELOG.md`,
},
],
title: 'Help',
},
{
items: [
{
description: 'AI Commit CLI',
openExternal: true,
title: '💌 Lobe Commit',
url: 'https://github.com/lobehub/lobe-commit',
},
{
description: 'Lint Config',
openExternal: true,
title: '📐 Lobe Lint',
url: 'https://github.com/lobehub/lobe-lint',
},
],
title: 'More Products',
},
];

View File

@ -0,0 +1,24 @@
import { Footer as F } from '@lobehub/ui';
import { memo, useEffect, useRef } from 'react';
import { DivProps } from '@/types/index';
import { columns } from './data';
import { useStyles } from './style';
const Footer = memo<DivProps>(({ className, ...props }) => {
const { cx, styles } = useStyles();
const footerReference: any = useRef<HTMLElement>();
useEffect(() => {
const footer = gradioApp().querySelector('#footer');
if (footer) footerReference.current?.append(footer);
}, []);
return (
<div className={cx(styles.footer, className)} {...props}>
<F bottom={<div ref={footerReference} />} columns={columns} />
</div>
);
});
export default Footer;

View File

@ -0,0 +1,9 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css }) => ({
footer: css`
footer {
display: block !important;
}
`,
}));

View File

@ -0,0 +1,88 @@
import { GithubOutlined } from '@ant-design/icons';
import { ActionIcon } from '@lobehub/ui';
import { Modal, Popover, Space } from 'antd';
import { Bold, Github, LucideIcon, Moon, Settings2, Sun } from 'lucide-react';
import qs from 'query-string';
import { memo, useCallback, useState } from 'react';
import { shallow } from 'zustand/shallow';
import Giscus from '@/slots/Giscus';
import Setting from '@/slots/Setting';
import { useAppStore } from '@/store';
const CivitaiLogo: LucideIcon = ({ size }) => (
<svg fill="currentColor" height={size} viewBox="0 0 16 16" width={size}>
<path d="M2 4.5L8 1l6 3.5v7L8 15l-6-3.5v-7zm6-1.194L3.976 5.653v4.694L8 12.694l4.024-2.347V5.653L8 3.306zm0 1.589l2.662 1.552v.824H9.25L8 6.54l-1.25.73v1.458L8 9.46l1.25-.73h1.412v.824L8 11.105 5.338 9.553V6.447L8 4.895z" />
</svg>
);
interface ActionsProps {
themeMode: 'dark' | 'light';
}
const Actions = memo<ActionsProps>(() => {
const [isModalOpen, setIsModalOpen] = useState(false);
const themeMode = useAppStore((st) => st.themeMode, shallow);
const handleSetTheme = useCallback(() => {
const theme = themeMode === 'light' ? 'dark' : 'light';
const gradioURL = qs.parseUrl(window.location.href);
gradioURL.query.__theme = theme;
window.location.replace(qs.stringifyUrl(gradioURL));
}, [themeMode]);
const showModal = () => setIsModalOpen(true);
const handleCancel = () => setIsModalOpen(false);
return (
<>
<Space.Compact>
<a href="https://civitai.com/" rel="noreferrer" target="_blank">
<ActionIcon icon={CivitaiLogo} title="Civitai" />
</a>
<a
href="https://www.birme.net/?target_width=512&target_height=512"
rel="noreferrer"
target="_blank"
>
<ActionIcon icon={Bold} title="Birme" />
</a>
<ActionIcon icon={Github} onClick={showModal} title="Feedback" />
<Popover
content={<Setting />}
title={<h3 style={{ lineHeight: 2, margin: 0 }}> Setting</h3>}
trigger="click"
>
<ActionIcon icon={Settings2} title="Setting" />
</Popover>
<ActionIcon
icon={themeMode === 'light' ? Sun : Moon}
onClick={handleSetTheme}
title="Switch Theme"
/>
</Space.Compact>
<Modal
footer={false}
onCancel={handleCancel}
open={isModalOpen}
title={
<a
href="https://github.com/canisminor1990/sd-webui-kitchen-theme"
rel="noreferrer"
target="_blank"
>
<Space>
<GithubOutlined />
{'canisminor1990/sd-webui-kitchen-theme'}
</Space>
</a>
}
>
<Giscus themeMode={themeMode} />
</Modal>
</>
);
});
export default Actions;

42
src/pages/Header/Nav.tsx Normal file
View File

@ -0,0 +1,42 @@
import { TabsNav, type TabsNavProps } from '@lobehub/ui';
import { memo, useEffect, useState } from 'react';
const hideOriganlNav = () => {
(gradioApp().querySelector('#tabs > .tab-nav:first-child') as HTMLDivElement).style.display =
'none';
};
const getNavButtons = (): HTMLButtonElement[] =>
Array.prototype.slice.call(
gradioApp().querySelectorAll(
'#tabs > .tab-nav:first-child button',
) as NodeListOf<HTMLButtonElement>,
);
const onChange: TabsNavProps['onChange'] = (activeKey) => {
const buttons = getNavButtons();
buttons[Number(activeKey)]?.click();
};
const Nav = memo(() => {
const [items, setItems] = useState<TabsNavProps['items']>([]);
useEffect(() => {
hideOriganlNav();
const buttons = getNavButtons();
const list: TabsNavProps['items'] = buttons.map(
(button: HTMLButtonElement | any, index: number) => {
button.id = `kitchen-nav-${index}`;
return {
key: String(index),
label: button.textContent,
};
},
);
setItems(list.filter(Boolean));
}, []);
return <TabsNav items={items} onChange={onChange} />;
});
export default Nav;

View File

@ -0,0 +1,37 @@
import { Header as H } from '@lobehub/ui';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { useAppStore } from '@/store';
import { DivProps } from '@/types/index';
import Actions from './Actions';
import Logo from './Logo';
import Nav from './Nav';
const Header = memo<DivProps>(({ children }) => {
const themeMode = useAppStore((st) => st.themeMode, shallow);
return (
<H
actions={<Actions themeMode={themeMode} />}
logo={
<a
href="https://github.com/canisminor1990/sd-webui-kitchen-theme"
rel="noreferrer"
target="_blank"
>
<Logo themeMode={themeMode} />
</a>
}
nav={
<>
<Nav />
{children}
</>
}
/>
);
});
export default Header;

View File

@ -0,0 +1,4 @@
export const darkLogo =
'https://gw.alipayobjects.com/zos/bmw-prod/9ecb2822-1592-4cb0-a087-ce0097fef2ca.svg';
export const lightLogo =
'https://gw.alipayobjects.com/zos/bmw-prod/e146116d-c65a-4306-a3d2-bb8d05e1c49b.svg';

View File

@ -0,0 +1,22 @@
import { DivProps } from '@lobehub/ui';
import { memo, useEffect, useRef } from 'react';
import PromptGroup from '@/slots/PromptEditor';
const Inner = memo<DivProps>(() => {
const sidebarReference: any = useRef<HTMLElement>();
useEffect(() => {
const sidebar = gradioApp().querySelector('#quicksettings');
if (sidebar) sidebarReference.current?.append(sidebar);
}, []);
return (
<>
<PromptGroup />
<div ref={sidebarReference} />
</>
);
});
export default Inner;

View File

@ -0,0 +1,49 @@
import { DivProps, DraggablePanel, LayoutSidebarInner } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useState } from 'react';
import { useAppStore } from '@/store';
import Inner from './Inner';
import { useStyles } from './style';
export interface QuickSettingSidebarProps extends DivProps {
headerHeight: number;
}
const QuickSettingSidebar = memo<QuickSettingSidebarProps>(({ headerHeight }) => {
const setting = useAppStore((st) => st.setting, isEqual);
const [mode] = useState<'fixed' | 'float'>(setting.sidebarFixedMode);
const [expand, setExpand] = useState<boolean>(setting.sidebarExpand);
const { mobile } = useResponsive();
const { styles } = useStyles({ headerHeight });
useEffect(() => {
if (mobile) setExpand(false);
}, []);
return (
<DraggablePanel
defaultSize={{ width: setting.sidebarWidth }}
expand={expand}
minWidth={setting.sidebarWidth}
mode={mode}
onExpandChange={setExpand}
pin={mode === 'fixed'}
placement="left"
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<LayoutSidebarInner>
<div className={styles.container}>
<Inner />
</div>
</LayoutSidebarInner>
</DraggablePanel>
);
});
export default QuickSettingSidebar;

View File

@ -0,0 +1,23 @@
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
export const useStyles = createStyles(
({ css, cx, stylish, token }, { headerHeight = 64 }: { headerHeight?: number }) => ({
container: cx(
stylish.blur,
css`
position: relative;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
height: calc(100vh - ${headerHeight}px);
padding: 16px;
background: ${rgba(token.colorBgLayout, 0.5)};
`,
),
}),
);

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

@ -0,0 +1,46 @@
import { LayoutHeader, LayoutMain, LayoutSidebar } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { memo, useEffect } from 'react';
import replaceIcon from '@/script/replaceIcon';
import { useAppStore } from '@/store';
import Content from './Content';
import ExtraNetworkSidebar from './ExtraNetworkSidebar';
import Footer from './Footer';
import Header from './Header';
import QuickSettingSidebar from './QuickSettingSidebar';
import { useStyles } from './style';
const HEADER_HEIGHT = 64;
const Index = memo(() => {
const { styles } = useStyles(HEADER_HEIGHT);
const setting = useAppStore((st) => st.setting, isEqual);
useEffect(() => {
if (setting.svgIcon) replaceIcon();
}, []);
return (
<>
<LayoutHeader headerHeight={HEADER_HEIGHT}>
<Header />
</LayoutHeader>
<LayoutMain>
<LayoutSidebar className={styles.sidebar} headerHeight={HEADER_HEIGHT}>
<QuickSettingSidebar headerHeight={HEADER_HEIGHT} />
</LayoutSidebar>
<Content />
{setting?.enableExtraNetworkSidebar && (
<LayoutSidebar className={styles.sidebar} headerHeight={HEADER_HEIGHT}>
<ExtraNetworkSidebar headerHeight={HEADER_HEIGHT} />
</LayoutSidebar>
)}
</LayoutMain>
<Footer />
</>
);
});
export default Index;

View File

@ -1,166 +0,0 @@
import {Spin} from 'antd';
import {useResponsive} from 'antd-style';
import {memo, useEffect, useRef, useState} from 'react';
import styled from 'styled-components';
import {shallow} from 'zustand/shallow';
import {Content, ExtraNetworkSidebar, Header, Sidebar} from '@/components';
import civitaiHelperFix from '@/script/civitaiHelperFix';
import dragablePanel from '@/script/draggablePanel';
import replaceIcon from '@/script/replaceIcon';
import {useAppStore} from '@/store';
const View = styled.div`
position: relative;
overflow: hidden;
display: flex;
flex: 1;
flex-direction: row !important;
`;
const MainView = styled.div`
position: relative;
overflow: hidden;
display: flex;
flex: 1;
flex-direction: column;
width: 100vw;
height: 100vh;
`;
const LoadingBox = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
`;
const App = memo(() => {
const [currentTab, setCurrentTab, setting] = useAppStore(
(st) => [st.currentTab, st.setCurrentTab, st.setting],
shallow,
);
const {mobile} = useResponsive();
const [loading, setLoading] = useState(true);
const [extraLoading, setExtraLoading] = useState(true);
const sidebarReference: any = useRef<HTMLElement>();
const mainReference: any = useRef<HTMLElement>();
const txt2imgExtraNetworkSidebarReference: any = useRef<HTMLElement>();
const img2imgExtraNetworkSidebarReference: any = useRef<HTMLElement>();
useEffect(() => {
onUiLoaded(() => {
// Content
const main = gradioApp().querySelector('.app');
if (main) mainReference.current?.append(main);
if (!mobile) dragablePanel();
// Sidebar
const sidebar = gradioApp().querySelector('#quicksettings');
if (sidebar) sidebarReference.current?.append(sidebar);
// ExtraNetworkSidebar
if (setting.enableExtraNetworkSidebar) {
const txt2imgExtraNetworks = gradioApp().querySelector('div#txt2img_extra_networks');
const img2imgExtraNetworks = gradioApp().querySelector('div#img2img_extra_networks');
if (txt2imgExtraNetworks && img2imgExtraNetworks) {
txt2imgExtraNetworkSidebarReference.current?.append(txt2imgExtraNetworks);
img2imgExtraNetworkSidebarReference.current?.append(img2imgExtraNetworks);
}
}
// Other
if (setting.svgIcon) replaceIcon();
setLoading(false);
});
onUiUpdate(() => {
setCurrentTab();
});
}, []);
useEffect(() => {
if (!loading && setting.enableExtraNetworkSidebar) {
if (document.querySelector('#txt2img_lora_cards')) {
civitaiHelperFix();
setExtraLoading(false);
return;
}
setTimeout(() => {
const t2indexButton: any = document.querySelector('#txt2img_extra_refresh');
const index2indexButton: any = document.querySelector('#img2img_extra_refresh');
t2indexButton.click();
index2indexButton.click();
setExtraLoading(false);
try {
const civitaiButton = document.querySelectorAll(
'button[title="Refresh Civitai Helper\'s additional buttons"]',
);
if (civitaiButton) {
civitaiButton.forEach((button: any) => (button.onclick = civitaiHelperFix));
}
civitaiHelperFix();
} catch (error) {
console.log(error);
}
}, 2000);
}
}, [loading]);
return (
<MainView>
<Header>
{loading && (
<LoadingBox>
<Spin size="small" />
</LoadingBox>
)}
</Header>
<View>
<Sidebar>
{loading && (
<LoadingBox>
<Spin size="small" />
</LoadingBox>
)}
<div id="sidebar" ref={sidebarReference} style={loading ? {display: 'none'} : {}} />
</Sidebar>
<Content loading={loading}>
{loading && (
<LoadingBox>
<Spin size="large" tip="Loading" />
</LoadingBox>
)}
<div id="content" ref={mainReference} style={loading ? {display: 'none'} : {}} />
</Content>
{setting?.enableExtraNetworkSidebar && (
<ExtraNetworkSidebar>
{extraLoading && (
<LoadingBox>
<Spin size="small" />
</LoadingBox>
)}
<div style={extraLoading ? {display: 'none'} : {}}>
<div
id="txt2img-extra-netwrok-sidebar"
ref={txt2imgExtraNetworkSidebarReference}
style={currentTab === 'tab_img2img' ? {display: 'none'} : {}}
/>
<div
id="img2img-extra-netwrok-sidebar"
ref={img2imgExtraNetworkSidebarReference}
style={currentTab === 'tab_img2img' ? {} : {display: 'none'}}
/>
</div>
</ExtraNetworkSidebar>
)}
</View>
</MainView>
);
});
export default App;

View File

@ -1,77 +0,0 @@
import { createGlobalStyle } from 'antd-style';
const GlobalStyle = createGlobalStyle`
html,body {
--font-settings: "cv01", "tnum", "kern";
--font-variations: "opsz" auto, tabular-nums;
overflow-x: hidden;
margin: 0;
padding: 0;
font-family: ${({ theme }) => theme.fontFamily};
font-size: ${({ theme }) => theme.fontSize}px;
font-feature-settings: var(--font-settings);
font-variation-settings: var(--font-variations);
line-height: 1;
color: ${({ theme }) => theme.colorTextBase};
text-size-adjust: none;
text-rendering: optimizelegibility;
vertical-align: baseline;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
}
* {
box-sizing: border-box;
vertical-align: baseline;
}
::-webkit-scrollbar {
width: 0;
height: 4px;
background-color: transparent;
&-thumb {
background-color: transparent;
border-radius: 4px;
transition: background-color 500ms ${({ theme }) => theme.motionEaseOut};
}
&-corner {
display: none;
}
}
#root {
min-height: 100vh;
}
p {
text-align: justify;
word-wrap: break-word;
}
code {
font-family: ${({ theme }) => theme.fontFamilyCode} !important;
* {
font-family: inherit !important;
}
}
*:hover, *:focus {
::-webkit-scrollbar {
&-thumb {
background-color: ${({ theme }) => theme.colorFill};
}
}
}
`;
export default GlobalStyle;

View File

@ -1,72 +0,0 @@
import {ThemeProvider} from '@lobehub/ui';
import qs from 'query-string';
import {memo, useEffect, useState} from 'react';
import {createRoot} from 'react-dom/client';
import {shallow} from 'zustand/shallow';
import {useIsDarkMode} from '@/components/theme/useIsDarkMode';
import formatPrompt from '@/script/formatPrompt';
import promptBracketChecker from '@/script/promptBracketChecker';
import setupHead from '@/script/setupHead';
import {useAppStore} from '@/store';
import '@/theme/style.less';
import App from './App';
const Root = memo(() => {
const [onSetThemeMode, onInit] = useAppStore((st) => [st.onSetThemeMode, st.onInit], shallow);
const isDarkMode = useIsDarkMode();
const [appearance, setAppearance] = useState<'light' | 'dark'>('light');
const [first, setFirst] = useState(true);
useEffect(() => {
onInit();
}, []);
useEffect(() => {
const queryTheme: any = String(qs.parseUrl(window.location.href).query.__theme || '');
if (queryTheme) {
setAppearance(queryTheme as any);
document.body.classList.add(queryTheme);
onSetThemeMode(queryTheme);
return;
}
setAppearance(isDarkMode ? 'dark' : 'light');
document.body.classList.add(isDarkMode ? 'dark' : 'light');
onSetThemeMode(isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
useEffect(() => {
if (first) {
setFirst(false);
return;
}
window.location.reload();
}, [isDarkMode]);
return (
<ThemeProvider themeMode={appearance}>
<App />
</ThemeProvider>
);
});
document.addEventListener(
'DOMContentLoaded',
() => {
setupHead();
const root = document.createElement('div');
root.setAttribute('id', 'root');
try {
gradioApp()?.append(root);
} catch {
document.querySelector('gradio-app')?.append(root);
}
const client = createRoot(root);
client.render(<Root />);
},
{once: true},
);
onUiLoaded(() => {
formatPrompt();
promptBracketChecker();
});
export default () => false;

View File

@ -1,11 +0,0 @@
import { AliasToken } from 'antd/es/theme/interface';
const FONT_EMOJI = `"Segoe UI Emoji","Segoe UI Symbol","Apple Color Emoji","Twemoji Mozilla","Noto Color Emoji","Android Emoji"`;
const FONT_EN = `"HarmonyOS Sans","Segoe UI","SF Pro Display",-apple-system,BlinkMacSystemFont,Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif`;
const FONT_CN = `"HarmonyOS Sans SC","PingFang SC","Hiragino Sans GB","Microsoft Yahei UI","Microsoft Yahei","Source Han Sans CN",sans-serif`;
const FONT_CODE = `ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace`;
export const baseToken: Partial<AliasToken> = {
fontFamily: [FONT_EN, FONT_CN, FONT_EMOJI].join(','),
fontFamilyCode: [FONT_CODE, FONT_CN, FONT_EMOJI].join(','),
};

7
src/pages/style.ts Normal file
View File

@ -0,0 +1,7 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css }, headerHeight: number) => ({
sidebar: css`
height: calc(100vh - ${headerHeight}px);
`,
}));

View File

@ -0,0 +1,32 @@
import { memo, useEffect } from 'react';
import { useStyles } from './style';
interface GiscusProps {
themeMode: 'light' | 'dark';
}
const Giscus = memo<GiscusProps>(({ themeMode }) => {
const { styles, cx } = useStyles();
useEffect(() => {
// giscus
const giscus: HTMLScriptElement = document.createElement('script');
giscus.src = 'https://giscus.app/client.js';
giscus.dataset.repo = 'canisminor1990/sd-webui-kitchen-theme';
giscus.dataset.repoId = 'R_kgDOJCPcNg';
giscus.dataset.mapping = 'number';
giscus.dataset.term = '53';
giscus.dataset.reactionsEnabled = '1';
giscus.dataset.emitMetadata = '0';
giscus.dataset.inputPosition = 'bottom';
giscus.dataset.theme = themeMode;
giscus.dataset.lang = 'en';
giscus.crossOrigin = 'anonymous';
giscus.async = true;
document.querySelectorAll('head')[0].append(giscus);
}, []);
return <div className={cx('giscus', styles.container)} id="giscus" />;
});
export default Giscus;

View File

@ -0,0 +1,9 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css }) => ({
container: css`
overflow-x: hidden;
overflow-y: auto;
height: 60vh;
`,
}));

View File

@ -0,0 +1,101 @@
import { memo, useCallback, useState } from 'react';
import styled from 'styled-components';
import TagList, { PromptType, TagItem } from './TagList';
import { formatPrompt } from './utils';
/******************************************************
*********************** Style *************************
******************************************************/
const View = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`;
const ButtonGroup = styled.div`
display: flex;
gap: 8px;
`;
const Button = styled.button`
cursor: pointer;
position: relative;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding: var(--input-padding);
font-size: var(--input-text-size);
font-weight: var(--input-text-weight);
line-height: var(--line-sm);
background: var(--button-secondary-background-fill);
border: var(--button-border-width) solid var(--button-secondary-border-color);
border-radius: var(--input-radius);
`;
/******************************************************
************************* Dom *************************
******************************************************/
interface PromptProps {
type: PromptType;
}
const Prompt = memo<PromptProps>(({ type }) => {
const [tags, setTags] = useState<TagItem[]>([]);
const id =
type === 'positive' ? "[id$='2img_prompt'] textarea" : "[id$='2img_neg_prompt'] textarea";
const getValue = useCallback(() => {
try {
const textarea: HTMLTextAreaElement | any = get_uiCurrentTabContent().querySelector(id);
if (textarea) setTags(formatPrompt(textarea.value));
} catch (error) {
console.log(error);
}
}, []);
const setValue = useCallback(() => {
try {
const textarea: HTMLTextAreaElement | any = get_uiCurrentTabContent().querySelector(id);
if (textarea) textarea.value = tags.map((t) => t.text).join(', ');
updateInput(textarea);
} catch (error) {
console.log(error);
}
}, [tags]);
const setCurrentValue = useCallback((currentTags: TagItem[]) => {
try {
const textarea: HTMLTextAreaElement | any = get_uiCurrentTabContent().querySelector(id);
if (textarea) textarea.value = currentTags.map((t) => t.text).join(', ');
updateInput(textarea);
} catch (error) {
console.log(error);
}
}, []);
return (
<View>
<TagList setTags={setTags} setValue={setCurrentValue} tags={tags} type={type} />
<ButtonGroup>
<Button onClick={getValue} title="Load Prompt">
🔄
</Button>
<Button onClick={setValue} title="Set Prompt">
</Button>
</ButtonGroup>
</View>
);
});
export default Prompt;

View File

@ -35,7 +35,7 @@ const Desc = styled.div`
************************* Dom ************************* ************************* Dom *************************
******************************************************/ ******************************************************/
const PromptGroup = memo(() => { const PromptEditor = memo(() => {
return ( return (
<View> <View>
<Desc>Positive</Desc> <Desc>Positive</Desc>
@ -46,4 +46,4 @@ const PromptGroup = memo(() => {
); );
}); });
export default PromptGroup; export default PromptEditor;

View File

@ -0,0 +1,48 @@
import negativeData from '@/data/negative.json';
import positiveData from '@/data/positive.json';
import { Converter } from '@/script/formatPrompt';
import { TagItem } from './TagList';
export const genTagType = (tag: TagItem): TagItem => {
const newTag = tag;
if (newTag.text.includes('<lora')) {
newTag.className = 'ReactTags__lora';
} else if (newTag.text.includes('<hypernet')) {
newTag.className = 'ReactTags__hypernet';
} else if (newTag.text.includes('<embedding')) {
newTag.className = 'ReactTags__embedding';
} else {
newTag.className = undefined;
}
return newTag;
};
export const formatPrompt = (value: string) => {
const text = Converter.convertStr(value);
const textArray = Converter.convertStr2Array(text).map((item) => {
if (item.includes('<')) return item;
const newItem = item
.replaceAll(/\s+/g, ' ')
.replaceAll(/|\.\|。/g, ',')
.replaceAll(/“||”|"|\/'/g, '')
.replaceAll(', ', ',')
.replaceAll(',,', ',')
.replaceAll(',', ', ');
return Converter.convertStr2Array(newItem).join(', ');
});
return textArray.map((tag) => genTagType({ id: tag, text: tag }));
};
const genSuggestions = (array: string[]) =>
array.map((text) => {
return {
id: text,
text,
};
});
export const suggestions = {
negative: genSuggestions(negativeData),
positive: genSuggestions(positiveData),
};

103
src/slots/Setting/index.tsx Normal file
View File

@ -0,0 +1,103 @@
import { Button, Divider, Form, InputNumber, Segmented, Space, Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { memo, useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { WebuiSetting, defaultSetting, useAppStore } from '@/store';
import { useStyles } from './style';
const { Item } = Form;
const Setting = memo(() => {
const setting = useAppStore((st) => st.setting, isEqual);
const onSetSetting = useAppStore((st) => st.onSetSetting, shallow);
const { styles } = useStyles();
const onReset = useCallback(() => {
onSetSetting(defaultSetting);
(gradioApp().querySelector('#settings_restart_gradio') as HTMLButtonElement)?.click();
}, []);
const onFinish = useCallback((value: WebuiSetting) => {
onSetSetting(value);
(gradioApp().querySelector('#settings_restart_gradio') as HTMLButtonElement)?.click();
}, []);
return (
<Form
initialValues={setting}
layout="horizontal"
onFinish={onFinish}
size="small"
style={{ maxWidth: 320 }}
>
<Divider style={{ margin: '4px 0 8px' }} />
<title className={styles.title}>Promot Textarea</title>
<Item className={styles.item} label="Display mode" name="promotTextarea">
<Segmented options={['scroll', 'resizable']} />
</Item>
<Divider style={{ margin: '4px 0 8px' }} />
<title className={styles.title}>Sidebar</title>
<Item
className={styles.item}
label="Default expand"
name="sidebarExpand"
valuePropName="checked"
>
<Switch />
</Item>
<Item className={styles.item} label="Display mode" name="sidebarFixedMode">
<Segmented options={['fixed', 'float']} />
</Item>
<Item className={styles.item} label="Default width" name="sidebarWidth">
<InputNumber />
</Item>
<Divider style={{ margin: '4px 0 8px' }} />
<title className={styles.title}>ExtraNetwork Sidebar</title>
<Item
className={styles.item}
label="Enable"
name="enableExtraNetworkSidebar"
valuePropName="checked"
>
<Switch />
</Item>
<Item className={styles.item} label="Display mode" name="extraNetworkFixedMode">
<Segmented options={['fixed', 'float']} />
</Item>
<Item
className={styles.item}
label="Default expand"
name="extraNetworkSidebarExpand"
valuePropName="checked"
>
<Switch />
</Item>
<Item className={styles.item} label="Default width" name="extraNetworkSidebarWidth">
<InputNumber />
</Item>
<Item className={styles.item} label="Default card size" name="extraNetworkCardSize">
<InputNumber />
</Item>
<Divider style={{ margin: '4px 0 8px' }} />
<title className={styles.title}>Other</title>
<Item className={styles.item} label="Use svg icons" name="svgIcon" valuePropName="checked">
<Switch />
</Item>
<Divider style={{ margin: '4px 0 16px' }} />
<Item className={styles.item}>
<Space>
<Button htmlType="button" onClick={onReset} style={{ borderRadius: 4 }}>
Reset
</Button>
<Button htmlType="submit" style={{ borderRadius: 4 }} type="primary">
Apply and restart UI
</Button>
</Space>
</Item>
</Form>
);
});
export default Setting;

View File

@ -0,0 +1,19 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css }) => ({
item: css`
.ant-row {
justify-content: space-between;
> div {
flex: unset !important;
flex-grow: unset !important;
}
}
`,
title: css`
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
`,
}));

14
src/types/global.d.ts vendored
View File

@ -1,6 +1,14 @@
import {Theme as AntdStyleTheme} from 'antd-style'; import type { LobeCustomStylish, LobeCustomToken } from '@lobehub/ui';
import 'antd-style';
import { AntdToken } from 'antd-style/lib/types/theme';
declare module 'antd-style' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomToken extends LobeCustomToken {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomStylish extends LobeCustomStylish {}
}
declare module 'styled-components' { declare module 'styled-components' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DefaultTheme extends AntdToken, LobeCustomToken {}
export interface DefaultTheme extends AntdStyleTheme {}
} }

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

354
style.css

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,27 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext",
"module": "esnext",
"baseUrl": ".", "baseUrl": ".",
"declaration": true, "declaration": true,
"downlevelIteration": true, "downlevelIteration": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"importHelpers": true,
"noEmit": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true,
"strict": true "strict": true
}, },
"exclude": ["javascript"], "exclude": ["javascript"],
"extends": "./src/.umi/tsconfig.json",
"include": ["src", "typings.d.ts", "*.ts"] "include": ["src", "typings", "*.ts", "*.d.ts", "*.tsx", "vite.config.ts"]
} }

138
typings.d.ts vendored
View File

@ -1 +1,137 @@
import 'umi/typings'; // This file is generated by Umi automatically
// DO NOT CHANGE IT MANUALLY!
type CSSModuleClasses = { readonly [key: string]: string };
declare module '*.css' {
const classes: CSSModuleClasses;
export default classes;
}
declare module '*.scss' {
const classes: CSSModuleClasses;
export default classes;
}
declare module '*.sass' {
const classes: CSSModuleClasses;
export default classes;
}
declare module '*.less' {
const classes: CSSModuleClasses;
export default classes;
}
declare module '*.styl' {
const classes: CSSModuleClasses;
export default classes;
}
declare module '*.stylus' {
const classes: CSSModuleClasses;
export default classes;
}
// images
declare module '*.jpg' {
const source: string;
export default source;
}
declare module '*.jpeg' {
const source: string;
export default source;
}
declare module '*.png' {
const source: string;
export default source;
}
declare module '*.gif' {
const source: string;
export default source;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
const source: string;
export default source;
}
declare module '*.ico' {
const source: string;
export default source;
}
declare module '*.webp' {
const source: string;
export default source;
}
declare module '*.avif' {
const source: string;
export default source;
}
// media
declare module '*.mp4' {
const source: string;
export default source;
}
declare module '*.webm' {
const source: string;
export default source;
}
declare module '*.ogg' {
const source: string;
export default source;
}
declare module '*.mp3' {
const source: string;
export default source;
}
declare module '*.wav' {
const source: string;
export default source;
}
declare module '*.flac' {
const source: string;
export default source;
}
declare module '*.aac' {
const source: string;
export default source;
}
// fonts
declare module '*.woff' {
const source: string;
export default source;
}
declare module '*.woff2' {
const source: string;
export default source;
}
declare module '*.eot' {
const source: string;
export default source;
}
declare module '*.ttf' {
const source: string;
export default source;
}
declare module '*.otf' {
const source: string;
export default source;
}
// other
declare module '*.wasm' {
const initWasm: (options: WebAssembly.Imports) => Promise<WebAssembly.Exports>;
export default initWasm;
}
declare module '*.webmanifest' {
const source: string;
export default source;
}
declare module '*.pdf' {
const source: string;
export default source;
}
declare module '*.txt' {
const source: string;
export default source;
}

85
vite.config.ts Normal file
View File

@ -0,0 +1,85 @@
import react from '@vitejs/plugin-react';
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
export default defineConfig({
base: 'dev',
build: {
outDir: './',
rollupOptions: {
input: resolve(__dirname, 'src/main.tsx'),
output: {
assetFileNames: `javascript/[name].[ext]`,
chunkFileNames: `javascript/[name].js`,
entryFileNames: `javascript/[name].js`,
},
},
},
plugins: [
react({
babel: {
plugins: ['@babel/plugin-syntax-import-assertions'],
},
}),
{
configureServer: (server) => {
server.middlewares.use((_request, res, next) => {
res.setHeader('Cross-Origin-Embedder-Policy', 'unsafe-none');
res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-non');
next();
});
},
name: 'configure-response-headers',
},
{
configureServer: (server) => {
server.middlewares.use(async(_request, res, next): Promise<void> => {
if (
_request.originalUrl === '/dev' ||
_request.originalUrl === '/dev?__theme=dark' ||
_request.originalUrl === '/dev?__theme=light'
) {
const response = await fetch('http://127.0.0.1:7860/');
let updatedResponse = await response.text();
const toAdd = `
<script type="module" src="/dev/src/_react_refresh.js"></script>
<script type="module" src="/dev/src/main.tsx"></script>
`;
// replace </body> with </body><script type="module" src="/main.js"></script>
updatedResponse = updatedResponse.replace('</body>', `</body>${toAdd}`);
// Set the modified response
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('charset', 'utf8');
res.end(updatedResponse);
return;
}
// Continue to the next middleware
next();
});
},
name: 'route-default-to-index',
},
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
server: {
host: '127.0.0.1',
port: 5173,
proxy: {
'/queue/join': {
target: 'ws://127.0.0.1:7860',
ws: true,
},
'^(?!.*dev).*$': 'http://127.0.0.1:7860',
},
},
});