make model switch work with toast

This commit is contained in:
Sanster
2022-04-17 23:31:12 +08:00
parent 205286a414
commit f7e1e073dc
18 changed files with 447 additions and 28 deletions

View File

@@ -1,4 +1,4 @@
import { Setting } from '../store/Atoms'
import { Settings } from '../store/Atoms'
import { dataURItoBlob } from '../utils'
export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}`
@@ -6,7 +6,7 @@ export const API_ENDPOINT = `${process.env.REACT_APP_INPAINTING_URL}`
export default async function inpaint(
imageFile: File,
maskBase64: string,
settings: Setting,
settings: Settings,
sizeLimit?: string
) {
// 1080, 2000, Original
@@ -43,8 +43,20 @@ export default async function inpaint(
export function switchModel(name: string) {
const fd = new FormData()
fd.append('name', name)
return fetch(`${API_ENDPOINT}/switch_model`, {
return fetch(`${API_ENDPOINT}/model`, {
method: 'POST',
body: fd,
})
}
export function currentModel() {
return fetch(`${API_ENDPOINT}/model`, {
method: 'GET',
})
}
export function modelDownloaded(name: string) {
return fetch(`${API_ENDPOINT}/model_downloaded/${name}`, {
method: 'GET',
})
}

View File

@@ -1,26 +1,28 @@
import React from 'react'
import { useRecoilState } from 'recoil'
import { switchModel } from '../../adapters/inpainting'
import { settingState } from '../../store/Atoms'
import Modal from '../shared/Modal'
import HDSettingBlock from './HDSettingBlock'
import ModelSettingBlock from './ModelSettingBlock'
export default function SettingModal() {
interface SettingModalProps {
onClose: () => void
}
export default function SettingModal(props: SettingModalProps) {
const { onClose } = props
const [setting, setSettingState] = useRecoilState(settingState)
const onClose = () => {
const handleOnClose = () => {
setSettingState(old => {
return { ...old, show: false }
})
switchModel(setting.model)
onClose()
}
return (
<Modal
onClose={onClose}
onClose={handleOnClose}
title="Settings"
className="modal-setting"
show={setting.show}

View File

@@ -1,18 +1,99 @@
import React from 'react'
import React, { useEffect } from 'react'
import { useRecoilState } from 'recoil'
import Editor from './Editor/Editor'
import ShortcutsModal from './Shortcuts/ShortcutsModal'
import SettingModal from './Settings/SettingsModal'
import Toast from './shared/Toast'
import { Settings, settingState, toastState } from '../store/Atoms'
import {
currentModel,
modelDownloaded,
switchModel,
} from '../adapters/inpainting'
import { AIModel } from './Settings/ModelSettingBlock'
interface WorkspaceProps {
file: File
}
const Workspace = ({ file }: WorkspaceProps) => {
const [settings, setSettingState] = useRecoilState(settingState)
const [toastVal, setToastState] = useRecoilState(toastState)
const onSettingClose = async () => {
const curModel = await currentModel().then(res => res.text())
if (curModel === settings.model) {
return
}
const downloaded = await modelDownloaded(settings.model).then(res =>
res.text()
)
const { model } = settings
let loadingMessage = `Switching to ${model} model`
let loadingDuration = 3000
if (downloaded === 'False') {
loadingMessage = `Downloading ${model} model, this may take a while`
loadingDuration = 9999999999
}
setToastState({
open: true,
desc: loadingMessage,
state: 'loading',
duration: loadingDuration,
})
switchModel(model)
.then(res => {
if (res.ok) {
setToastState({
open: true,
desc: `Switch to ${model} model success`,
state: 'success',
duration: 3000,
})
} else {
throw new Error('Server error')
}
})
.catch(() => {
setToastState({
open: true,
desc: `Switch to ${model} model failed`,
state: 'error',
duration: 3000,
})
setSettingState(old => {
return { ...old, model: curModel as AIModel }
})
})
}
useEffect(() => {
currentModel()
.then(res => res.text())
.then(model => {
setSettingState(old => {
return { ...old, model: model as AIModel }
})
})
}, [])
return (
<>
<Editor file={file} />
<SettingModal />
<SettingModal onClose={onSettingClose} />
<ShortcutsModal />
<Toast
{...toastVal}
onOpenChange={(open: boolean) => {
setToastState(old => {
return { ...old, open }
})
}}
/>
</>
)
}

View File

@@ -0,0 +1,83 @@
.toast-viewpoint {
position: fixed;
top: 48px;
right: 0;
display: flex;
flex-direction: row;
padding: 25px;
gap: 10px;
max-width: 100vw;
margin: 0;
z-index: 999999;
&:focus-visible {
outline: none;
}
}
.toast-root {
border: 1px solid var(--border-color-light);
background-color: var(--page-bg);
border-radius: 0.6rem;
padding: 15px;
display: flex;
align-items: center;
gap: 12px;
&[data-state='open'] {
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
&[data-state='close'] {
animation: opacityReveal 100ms ease-in forwards;
}
&[data-state='cancel'] {
transform: translateX(0);
animation: transform 100ms ease-out;
}
&.error {
border: 1px solid var(--error-color);
}
&.success {
border: 1px solid var(--success-color);
}
}
.error-icon {
height: 24px;
width: 24px;
color: var(--error-color);
}
.success-icon {
height: 24px;
width: 24px;
color: var(--success-color);
}
.loading-icon {
display: flex;
align-items: center;
animation-name: spin;
animation-duration: 1500ms;
animation-iteration-count: infinite;
transform-origin: center center;
animation-timing-function: linear;
}
.toast-icon {
display: flex;
align-items: center;
}
.toast-desc {
display: flex;
align-items: center;
margin: 0;
color: var(--text-color);
min-width: 240px;
}

View File

@@ -0,0 +1,81 @@
import * as React from 'react'
import * as ToastPrimitive from '@radix-ui/react-toast'
import { ToastProps } from '@radix-ui/react-toast'
import { CheckIcon, ExclamationCircleIcon } from '@heroicons/react/outline'
const LoadingIcon = () => {
return (
<span className="loading-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="2" x2="12" y2="6" />
<line x1="12" y1="18" x2="12" y2="22" />
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76" />
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07" />
<line x1="2" y1="12" x2="6" y2="12" />
<line x1="18" y1="12" x2="22" y2="12" />
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24" />
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93" />
</svg>
</span>
)
}
export type ToastState = 'default' | 'error' | 'loading' | 'success'
interface MyToastProps extends ToastProps {
desc: string
state?: ToastState
}
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitive.Root>,
MyToastProps
>((props, forwardedRef) => {
const { state, desc, ...itemProps } = props
const getIcon = () => {
switch (state) {
case 'error':
return <ExclamationCircleIcon className="error-icon" />
case 'success':
return <CheckIcon className="success-icon" />
case 'loading':
return <LoadingIcon />
default:
return <></>
}
}
return (
<ToastPrimitive.Provider>
<ToastPrimitive.Root
{...itemProps}
ref={forwardedRef}
className={`toast-root ${state}`}
>
<div className="toast-icon">{getIcon()}</div>
<ToastPrimitive.Description className="toast-desc">
{desc}
</ToastPrimitive.Description>
</ToastPrimitive.Root>
<ToastPrimitive.Viewport className="toast-viewpoint" />
</ToastPrimitive.Provider>
)
})
Toast.defaultProps = {
desc: '',
state: 'loading',
}
export default Toast

View File

@@ -1,18 +1,36 @@
import { atom } from 'recoil'
import { HDStrategy } from '../components/Settings/HDSettingBlock'
import { AIModel } from '../components/Settings/ModelSettingBlock'
import { ToastState } from '../components/shared/Toast'
export const fileState = atom<File | undefined>({
key: 'fileState',
default: undefined,
})
interface ToastAtomState {
open: boolean
desc: string
state: ToastState
duration: number
}
export const toastState = atom<ToastAtomState>({
key: 'toastState',
default: {
open: false,
desc: '',
state: 'default',
duration: 3000,
},
})
export const shortcutsState = atom<boolean>({
key: 'shortcutsState',
default: false,
})
export interface Setting {
export interface Settings {
show: boolean
saveImageBesideOrigin: boolean
model: AIModel
@@ -27,16 +45,18 @@ export interface Setting {
ldmSteps: number
}
export const settingState = atom<Setting>({
export const settingStateDefault = {
show: false,
saveImageBesideOrigin: false,
model: AIModel.LAMA,
ldmSteps: 50,
hdStrategy: HDStrategy.RESIZE,
hdStrategyResizeLimit: 2048,
hdStrategyCropTrigerSize: 2048,
hdStrategyCropMargin: 128,
}
export const settingState = atom<Settings>({
key: 'settingsState',
default: {
show: false,
saveImageBesideOrigin: false,
model: AIModel.LAMA,
hdStrategy: HDStrategy.RESIZE,
hdStrategyResizeLimit: 2048,
hdStrategyCropTrigerSize: 2048,
hdStrategyCropMargin: 128,
ldmSteps: 50,
},
default: settingStateDefault,
})

View File

@@ -37,3 +37,21 @@
transform: translateY(0);
}
}
@keyframes slideIn {
0% {
transform: translateX(calc(100% + 25px));
}
100% {
transform: translateX(0);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -7,6 +7,10 @@
--yellow-accent: #ffcc00;
--link-color: rgb(0, 0, 0);
--border-color: rgb(100, 100, 120);
--border-color-light: rgba(100, 100, 120, 0.5);
--error-color: rgb(239, 68, 68);
--success-color: rgb(16, 185, 129);
// Editor
--editor-toolkit-bg: rgba(255, 255, 255, 0.5);

View File

@@ -7,6 +7,7 @@
--yellow-accent: #ffcc00;
--link-color: var(--yellow-accent);
--border-color: rgb(100, 100, 120);
--border-color-light: rgba(102, 102, 102);
// Editor
--editor-toolkit-bg: rgba(0, 0, 0, 0.5);

View File

@@ -20,6 +20,7 @@
@use '../components/shared/Selector';
@use '../components/shared/Switch';
@use '../components/shared/NumberInput';
@use '../components/shared/Toast';
// Main CSS
*,