ChatGPT-Next-Web/app/components/sd/sd-panel.tsx

320 lines
9.2 KiB
TypeScript

import { IconButton } from '@/app/components/button';
import { Select } from '@/app/components/ui-lib';
import Locale from '@/app/locales';
import { useSdStore } from '@/app/store/sd';
import clsx from 'clsx';
import React from 'react';
import styles from './sd-panel.module.scss';
export const params = [
{
name: Locale.SdPanel.Prompt,
value: 'prompt',
type: 'textarea',
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
required: true,
},
{
name: Locale.SdPanel.ModelVersion,
value: 'model',
type: 'select',
default: 'sd3-medium',
support: ['sd3'],
options: [
{ name: 'SD3 Medium', value: 'sd3-medium' },
{ name: 'SD3 Large', value: 'sd3-large' },
{ name: 'SD3 Large Turbo', value: 'sd3-large-turbo' },
],
},
{
name: Locale.SdPanel.NegativePrompt,
value: 'negative_prompt',
type: 'textarea',
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
},
{
name: Locale.SdPanel.AspectRatio,
value: 'aspect_ratio',
type: 'select',
default: '1:1',
options: [
{ name: '1:1', value: '1:1' },
{ name: '16:9', value: '16:9' },
{ name: '21:9', value: '21:9' },
{ name: '2:3', value: '2:3' },
{ name: '3:2', value: '3:2' },
{ name: '4:5', value: '4:5' },
{ name: '5:4', value: '5:4' },
{ name: '9:16', value: '9:16' },
{ name: '9:21', value: '9:21' },
],
},
{
name: Locale.SdPanel.ImageStyle,
value: 'style',
type: 'select',
default: '3d-model',
support: ['core'],
options: [
{ name: Locale.SdPanel.Styles.D3Model, value: '3d-model' },
{ name: Locale.SdPanel.Styles.AnalogFilm, value: 'analog-film' },
{ name: Locale.SdPanel.Styles.Anime, value: 'anime' },
{ name: Locale.SdPanel.Styles.Cinematic, value: 'cinematic' },
{ name: Locale.SdPanel.Styles.ComicBook, value: 'comic-book' },
{ name: Locale.SdPanel.Styles.DigitalArt, value: 'digital-art' },
{ name: Locale.SdPanel.Styles.Enhance, value: 'enhance' },
{ name: Locale.SdPanel.Styles.FantasyArt, value: 'fantasy-art' },
{ name: Locale.SdPanel.Styles.Isometric, value: 'isometric' },
{ name: Locale.SdPanel.Styles.LineArt, value: 'line-art' },
{ name: Locale.SdPanel.Styles.LowPoly, value: 'low-poly' },
{
name: Locale.SdPanel.Styles.ModelingCompound,
value: 'modeling-compound',
},
{ name: Locale.SdPanel.Styles.NeonPunk, value: 'neon-punk' },
{ name: Locale.SdPanel.Styles.Origami, value: 'origami' },
{ name: Locale.SdPanel.Styles.Photographic, value: 'photographic' },
{ name: Locale.SdPanel.Styles.PixelArt, value: 'pixel-art' },
{ name: Locale.SdPanel.Styles.TileTexture, value: 'tile-texture' },
],
},
{
name: 'Seed',
value: 'seed',
type: 'number',
default: 0,
min: 0,
max: 4294967294,
},
{
name: Locale.SdPanel.OutFormat,
value: 'output_format',
type: 'select',
default: 'png',
options: [
{ name: 'PNG', value: 'png' },
{ name: 'JPEG', value: 'jpeg' },
{ name: 'WebP', value: 'webp' },
],
},
];
function sdCommonParams(model: string, data: any) {
return params.filter((item) => {
return !(item.support && !item.support.includes(model));
});
}
export const models = [
{
name: 'Stable Image Ultra',
value: 'ultra',
params: (data: any) => sdCommonParams('ultra', data),
},
{
name: 'Stable Image Core',
value: 'core',
params: (data: any) => sdCommonParams('core', data),
},
{
name: 'Stable Diffusion 3',
value: 'sd3',
params: (data: any) => {
return sdCommonParams('sd3', data).filter((item) => {
return !(
data.model === 'sd3-large-turbo' && item.value == 'negative_prompt'
);
});
},
},
];
export function ControlParamItem(props: {
title: string;
subTitle?: string;
required?: boolean;
children?: JSX.Element | JSX.Element[];
className?: string;
}) {
return (
<div className={clsx(styles['ctrl-param-item'], props.className)}>
<div className={styles['ctrl-param-item-header']}>
<div className={styles['ctrl-param-item-title']}>
<div>
{props.title}
{props.required && <span style={{ color: 'red' }}>*</span>}
</div>
</div>
</div>
{props.children}
{props.subTitle && (
<div className={styles['ctrl-param-item-sub-title']}>
{props.subTitle}
</div>
)}
</div>
);
}
export function ControlParam(props: {
columns: any[];
data: any;
onChange: (field: string, val: any) => void;
}) {
return (
<>
{props.columns?.map((item) => {
let element: null | JSX.Element;
switch (item.type) {
case 'textarea':
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<textarea
rows={item.rows || 3}
style={{ maxWidth: '100%', width: '100%', padding: '10px' }}
placeholder={item.placeholder}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
}}
value={props.data[item.value]}
>
</textarea>
</ControlParamItem>
);
break;
case 'select':
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<Select
aria-label={item.name}
value={props.data[item.value]}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
}}
>
{item.options.map((opt: any) => {
return (
<option value={opt.value} key={opt.value}>
{opt.name}
</option>
);
})}
</Select>
</ControlParamItem>
);
break;
case 'number':
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<input
aria-label={item.name}
type="number"
min={item.min}
max={item.max}
value={props.data[item.value] || 0}
onChange={(e) => {
props.onChange(item.value, Number.parseInt(e.currentTarget.value));
}}
/>
</ControlParamItem>
);
break;
default:
element = (
<ControlParamItem
title={item.name}
subTitle={item.sub}
required={item.required}
>
<input
aria-label={item.name}
type="text"
value={props.data[item.value]}
style={{ maxWidth: '100%', width: '100%' }}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
}}
/>
</ControlParamItem>
);
}
return <div key={item.value}>{element}</div>;
})}
</>
);
}
export function getModelParamBasicData(columns: any[], data: any, clearText?: boolean) {
const newParams: any = {};
columns.forEach((item: any) => {
if (clearText && ['text', 'textarea', 'number'].includes(item.type)) {
newParams[item.value] = item.default || '';
} else {
// @ts-ignore
newParams[item.value] = data[item.value] || item.default || '';
}
});
return newParams;
}
export function getParams(model: any, params: any) {
return models.find(m => m.value === model.value)?.params(params) || [];
}
export function SdPanel() {
const sdStore = useSdStore();
const currentModel = sdStore.currentModel;
const setCurrentModel = sdStore.setCurrentModel;
const params = sdStore.currentParams;
const setParams = sdStore.setCurrentParams;
const handleValueChange = (field: string, val: any) => {
setParams({
...params,
[field]: val,
});
};
const handleModelChange = (model: any) => {
setCurrentModel(model);
setParams(getModelParamBasicData(model.params({}), params));
};
return (
<>
<ControlParamItem title={Locale.SdPanel.AIModel}>
<div className={styles['ai-models']}>
{models.map((item) => {
return (
<IconButton
text={item.name}
key={item.value}
type={currentModel.value == item.value ? 'primary' : null}
shadow
onClick={() => handleModelChange(item)}
/>
);
})}
</div>
</ControlParamItem>
<ControlParam
columns={getParams?.(currentModel, params) as any[]}
data={params}
onChange={handleValueChange}
>
</ControlParam>
</>
);
}