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

357 lines
14 KiB
TypeScript

import type { Property } from 'csstype';
import { IconButton } from '@/app/components/button';
import { ChatAction } from '@/app/components/chat';
import chatStyles from '@/app/components/chat.module.scss';
import { WindowContent } from '@/app/components/home';
import homeStyles from '@/app/components/home.module.scss';
import styles from '@/app/components/sd/sd.module.scss';
import {
showConfirm,
showImageModal,
showModal,
} from '@/app/components/ui-lib';
import { getClientConfig } from '@/app/config/client';
import { Path } from '@/app/constant';
import DeleteIcon from '@/app/icons/clear.svg';
import CopyIcon from '@/app/icons/copy.svg';
import ErrorIcon from '@/app/icons/delete.svg';
import MaxIcon from '@/app/icons/max.svg';
import MinIcon from '@/app/icons/min.svg';
import PromptIcon from '@/app/icons/prompt.svg';
import ResetIcon from '@/app/icons/reload.svg';
import ReturnIcon from '@/app/icons/return.svg';
import SDIcon from '@/app/icons/sd.svg';
import LoadingIcon from '@/app/icons/three-dots.svg';
import Locale from '@/app/locales';
import { useAppConfig } from '@/app/store';
import { useSdStore } from '@/app/store/sd';
import {
copyToClipboard,
getMessageTextContent,
useMobileScreen,
} from '@/app/utils';
import { removeImage } from '@/app/utils/chat';
import clsx from 'clsx';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { params } from './sd-panel';
import { SideBar } from './sd-sidebar';
function getSdTaskStatus(item: any) {
let s: string;
let color: Property.Color | undefined;
switch (item.status) {
case 'success':
s = Locale.Sd.Status.Success;
color = 'green';
break;
case 'error':
s = Locale.Sd.Status.Error;
color = 'red';
break;
case 'wait':
s = Locale.Sd.Status.Wait;
color = 'yellow';
break;
case 'running':
s = Locale.Sd.Status.Running;
color = 'blue';
break;
default:
s = item.status.toUpperCase();
}
return (
<p className={styles['line-1']} title={item.error} style={{ color }}>
<span>
{Locale.Sd.Status.Name}
:
{s}
</span>
{item.status === 'error' && (
<span
className="clickable"
onClick={() => {
showModal({
title: Locale.Sd.Detail,
children: (
<div style={{ color, userSelect: 'text' }}>
{item.error}
</div>
),
});
}}
>
-
{' '}
{item.error}
</span>
)}
</p>
);
}
export function Sd() {
const isMobileScreen = useMobileScreen();
const navigate = useNavigate();
const location = useLocation();
const clientConfig = useMemo(() => getClientConfig(), []);
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
const config = useAppConfig();
const scrollRef = useRef<HTMLDivElement>(null);
const sdStore = useSdStore();
const [sdImages, setSdImages] = useState(sdStore.draw);
const isSd = location.pathname === Path.Sd;
useEffect(() => {
setSdImages(sdStore.draw);
}, [sdStore.currentId]);
return (
<>
<SideBar className={clsx({ [homeStyles['sidebar-show']]: isSd })} />
<WindowContent>
<div className={chatStyles.chat} key="1">
<div className="window-header" data-tauri-drag-region>
{isMobileScreen && (
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<ReturnIcon />}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={() => navigate(Path.Sd)}
/>
</div>
</div>
)}
<div
className={clsx(
'window-header-title',
chatStyles['chat-body-title'],
)}
>
<div className="window-header-main-title">Stability AI</div>
<div className="window-header-sub-title">
{Locale.Sd.SubTitle(sdImages.length || 0)}
</div>
</div>
<div className="window-actions">
{showMaxIcon && (
<div className="window-action-button">
<IconButton
aria={Locale.Chat.Actions.FullScreen}
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {
config.update(
config => (config.tightBorder = !config.tightBorder),
);
}}
/>
</div>
)}
{isMobileScreen && <SDIcon width={50} height={50} />}
</div>
</div>
<div className={chatStyles['chat-body']} ref={scrollRef}>
<div className={styles['sd-img-list']}>
{sdImages.length > 0 ? (
sdImages.map((item: any) => {
return (
<div
key={item.id}
style={{ display: 'flex' }}
className={styles['sd-img-item']}
>
{item.status === 'success'
? (
<img
className={styles.img}
src={item.img_data}
alt={item.id}
onClick={e =>
showImageModal(
item.img_data,
true,
isMobileScreen
? { width: '100%', height: 'fit-content' }
: { maxWidth: '100%', maxHeight: '100%' },
isMobileScreen
? { width: '100%', height: 'fit-content' }
: { width: '100%', height: '100%' },
)}
/>
)
: item.status === 'error'
? (
<div className={styles['pre-img']}>
<ErrorIcon />
</div>
)
: (
<div className={styles['pre-img']}>
<LoadingIcon />
</div>
)}
<div
style={{ marginLeft: '10px' }}
className={styles['sd-img-item-info']}
>
<p className={styles['line-1']}>
{Locale.SdPanel.Prompt}
:
{' '}
<span
className="clickable"
title={item.params.prompt}
onClick={() => {
showModal({
title: Locale.Sd.Detail,
children: (
<div style={{ userSelect: 'text' }}>
{item.params.prompt}
</div>
),
});
}}
>
{item.params.prompt}
</span>
</p>
<p>
{Locale.SdPanel.AIModel}
:
{item.model_name}
</p>
{getSdTaskStatus(item)}
<p>{item.created_at}</p>
<div className={chatStyles['chat-message-actions']}>
<div className={chatStyles['chat-input-actions']}>
<ChatAction
text={Locale.Sd.Actions.Params}
icon={<PromptIcon />}
onClick={() => {
showModal({
title: Locale.Sd.GenerateParams,
children: (
<div style={{ userSelect: 'text' }}>
{Object.keys(item.params).map((key) => {
let label = key;
let value = item.params[key];
switch (label) {
case 'prompt':
label = Locale.SdPanel.Prompt;
break;
case 'negative_prompt':
label
= Locale.SdPanel.NegativePrompt;
break;
case 'aspect_ratio':
label = Locale.SdPanel.AspectRatio;
break;
case 'seed':
label = 'Seed';
value = value || 0;
break;
case 'output_format':
label = Locale.SdPanel.OutFormat;
value = value?.toUpperCase();
break;
case 'style':
label = Locale.SdPanel.ImageStyle;
value = params
.find(
item =>
item.value === 'style',
)
?.options
?.find(
item => item.value === value,
)
?.name;
break;
default:
break;
}
return (
<div
key={key}
style={{ margin: '10px' }}
>
<strong>
{label}
:
{' '}
</strong>
{value}
</div>
);
})}
</div>
),
});
}}
/>
<ChatAction
text={Locale.Sd.Actions.Copy}
icon={<CopyIcon />}
onClick={() =>
copyToClipboard(
getMessageTextContent({
role: 'user',
content: item.params.prompt,
}),
)}
/>
<ChatAction
text={Locale.Sd.Actions.Retry}
icon={<ResetIcon />}
onClick={() => {
const reqData = {
model: item.model,
model_name: item.model_name,
status: 'wait',
params: { ...item.params },
created_at: new Date().toLocaleString(),
img_data: '',
};
sdStore.sendTask(reqData);
}}
/>
<ChatAction
text={Locale.Sd.Actions.Delete}
icon={<DeleteIcon />}
onClick={async () => {
if (
await showConfirm(Locale.Sd.Danger.Delete)
) {
// remove img_data + remove item in list
removeImage(item.img_data).finally(() => {
sdStore.draw = sdImages.filter(
(i: any) => i.id !== item.id,
);
sdStore.getNextId();
});
}
}}
/>
</div>
</div>
</div>
</div>
);
})
) : (
<div>{Locale.Sd.EmptyRecord}</div>
)}
</div>
</div>
</div>
</WindowContent>
</>
);
}