From 6226778cfbfa340eabaf64d84a24a9c60a43fe86 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sat, 18 Mar 2023 17:34:16 -0500 Subject: [PATCH] feat(gui): add error status to image card --- gui/src/client/api.ts | 4 +- gui/src/components/ImageHistory.tsx | 37 +++++++----- gui/src/components/{ => card}/ImageCard.tsx | 14 ++--- gui/src/components/{ => card}/LoadingCard.tsx | 30 ++++------ gui/src/components/card/RetryCard.tsx | 59 +++++++++++++++++++ gui/src/components/tab/Blend.tsx | 4 +- gui/src/components/tab/Img2Img.tsx | 4 +- gui/src/components/tab/Inpaint.tsx | 6 +- gui/src/components/tab/Txt2Img.tsx | 4 +- gui/src/components/tab/Upscale.tsx | 4 +- gui/src/state.ts | 43 ++++---------- 11 files changed, 124 insertions(+), 85 deletions(-) rename gui/src/components/{ => card}/ImageCard.tsx (95%) rename gui/src/components/{ => card}/LoadingCard.tsx (77%) create mode 100644 gui/src/components/card/RetryCard.tsx diff --git a/gui/src/client/api.ts b/gui/src/client/api.ts index 44414b1c..2f01b241 100644 --- a/gui/src/client/api.ts +++ b/gui/src/client/api.ts @@ -175,8 +175,8 @@ export interface ImageResponse { * Status response from the ready endpoint. */ export interface ReadyResponse { - cancel: boolean; - error: boolean; + cancelled: boolean; + failed: boolean; progress: number; ready: boolean; } diff --git a/gui/src/components/ImageHistory.tsx b/gui/src/components/ImageHistory.tsx index 713d3509..fad4d2b3 100644 --- a/gui/src/components/ImageHistory.tsx +++ b/gui/src/components/ImageHistory.tsx @@ -1,38 +1,45 @@ import { doesExist, mustExist } from '@apextoaster/js-utils'; import { Grid, Typography } from '@mui/material'; -import { useContext } from 'react'; +import { useContext, ReactNode } from 'react'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useStore } from 'zustand'; import { StateContext } from '../state.js'; -import { ImageCard } from './ImageCard.js'; -import { LoadingCard } from './LoadingCard.js'; +import { ImageCard } from './card/ImageCard.js'; +import { LoadingCard } from './card/LoadingCard.js'; +import { ErrorCard } from './card/RetryCard.js'; export function ImageHistory() { const history = useStore(mustExist(useContext(StateContext)), (state) => state.history); const limit = useStore(mustExist(useContext(StateContext)), (state) => state.limit); - const loading = useStore(mustExist(useContext(StateContext)), (state) => state.loading); // eslint-disable-next-line @typescript-eslint/unbound-method const removeHistory = useStore(mustExist(useContext(StateContext)), (state) => state.removeHistory); const { t } = useTranslation(); - const children = []; + const children: Array<[string, ReactNode]> = []; - if (loading.length > 0) { - children.push(...loading.map((item) => )); + if (history.length === 0) { + children.push(['empty', {t('history.empty')}]); } - if (history.length > 0) { - children.push(...history.map((item) => )); - } else { - if (doesExist(loading) === false) { - children.push({t('history.empty')}); + const limited = history.slice(0, limit); + for (const item of limited) { + const key = item.image.outputs[0].key; + + if (doesExist(item.ready) && item.ready.ready) { + if (item.ready.cancelled || item.ready.failed) { + children.push([key, ]); + continue; + } + + children.push([key, ]); + continue; } + + children.push([key, ]); } - const limited = children.slice(0, limit); - - return {limited.map((child, idx) => {child})}; + return {children.map(([key, child]) => {child})}; } diff --git a/gui/src/components/ImageCard.tsx b/gui/src/components/card/ImageCard.tsx similarity index 95% rename from gui/src/components/ImageCard.tsx rename to gui/src/components/card/ImageCard.tsx index a2c03326..10a2dd21 100644 --- a/gui/src/components/ImageCard.tsx +++ b/gui/src/components/card/ImageCard.tsx @@ -7,12 +7,12 @@ import { useTranslation } from 'react-i18next'; import { useHash } from 'react-use/lib/useHash'; import { useStore } from 'zustand'; -import { ImageResponse } from '../client/api.js'; -import { BLEND_SOURCES, ConfigContext, StateContext } from '../state.js'; -import { range, visibleIndex } from '../utils.js'; +import { ImageResponse } from '../../client/api.js'; +import { BLEND_SOURCES, ConfigContext, StateContext } from '../../state.js'; +import { range, visibleIndex } from '../../utils.js'; export interface ImageCardProps { - value: ImageResponse; + image: ImageResponse; onDelete?: (key: ImageResponse) => void; } @@ -24,8 +24,8 @@ export function GridItem(props: { xs: number; children: React.ReactNode }) { } export function ImageCard(props: ImageCardProps) { - const { value } = props; - const { params, outputs, size } = value; + const { image } = props; + const { params, outputs, size } = image; const [_hash, setHash] = useHash(); const [anchor, setAnchor] = useState>(); @@ -83,7 +83,7 @@ export function ImageCard(props: ImageCardProps) { function deleteImage() { if (doesExist(props.onDelete)) { - props.onDelete(value); + props.onDelete(image); } } diff --git a/gui/src/components/LoadingCard.tsx b/gui/src/components/card/LoadingCard.tsx similarity index 77% rename from gui/src/components/LoadingCard.tsx rename to gui/src/components/card/LoadingCard.tsx index 311088f8..73083464 100644 --- a/gui/src/components/LoadingCard.tsx +++ b/gui/src/components/card/LoadingCard.tsx @@ -7,36 +7,34 @@ import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; import { useStore } from 'zustand'; -import { ImageResponse } from '../client/api.js'; -import { POLL_TIME } from '../config.js'; -import { ClientContext, ConfigContext, StateContext } from '../state.js'; +import { ImageResponse } from '../../client/api.js'; +import { POLL_TIME } from '../../config.js'; +import { ClientContext, ConfigContext, StateContext } from '../../state.js'; const LOADING_PERCENT = 100; const LOADING_OVERAGE = 99; export interface LoadingCardProps { + image: ImageResponse; index: number; - loading: ImageResponse; } export function LoadingCard(props: LoadingCardProps) { - const { index, loading } = props; - const { steps } = props.loading.params; + const { image, index } = props; + const { steps } = props.image.params; const client = mustExist(React.useContext(ClientContext)); const { params } = mustExist(useContext(ConfigContext)); const state = mustExist(useContext(StateContext)); // eslint-disable-next-line @typescript-eslint/unbound-method - const clearLoading = useStore(state, (s) => s.clearLoading); - // eslint-disable-next-line @typescript-eslint/unbound-method - const pushHistory = useStore(state, (s) => s.pushHistory); + const removeHistory = useStore(state, (s) => s.removeHistory); // eslint-disable-next-line @typescript-eslint/unbound-method const setReady = useStore(state, (s) => s.setReady); const { t } = useTranslation(); - const cancel = useMutation(() => client.cancel(loading.outputs[index].key)); - const ready = useQuery(`ready-${loading.outputs[index].key}`, () => client.ready(loading.outputs[index].key), { + const cancel = useMutation(() => client.cancel(image.outputs[index].key)); + const ready = useQuery(`ready-${image.outputs[index].key}`, () => client.ready(image.outputs[index].key), { // data will always be ready without this, even if the API says its not cacheTime: 0, refetchInterval: POLL_TIME, @@ -86,17 +84,13 @@ export function LoadingCard(props: LoadingCardProps) { useEffect(() => { if (cancel.status === 'success') { - clearLoading(props.loading); + removeHistory(props.image); } }, [cancel.status]); useEffect(() => { - if (ready.status === 'success') { - if (ready.data.ready) { - pushHistory(props.loading); - } else { - setReady(props.loading, ready.data); - } + if (ready.status === 'success' && getReady()) { + setReady(props.image, ready.data); } }, [ready.status, getReady(), getProgress()]); diff --git a/gui/src/components/card/RetryCard.tsx b/gui/src/components/card/RetryCard.tsx new file mode 100644 index 00000000..501ce968 --- /dev/null +++ b/gui/src/components/card/RetryCard.tsx @@ -0,0 +1,59 @@ +import { mustExist } from '@apextoaster/js-utils'; +import { Box, Button, Card, CardContent, Typography } from '@mui/material'; +import { Stack } from '@mui/system'; +import * as React from 'react'; +import { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { useStore } from 'zustand'; + +import { ImageResponse, ReadyResponse } from '../../client/api.js'; +import { ClientContext, ConfigContext, StateContext } from '../../state.js'; + +export interface ErrorCardProps { + image: ImageResponse; + ready: ReadyResponse; +} + +export function ErrorCard(props: ErrorCardProps) { + const { image, ready } = props; + + const client = mustExist(React.useContext(ClientContext)); + const { params } = mustExist(useContext(ConfigContext)); + + const state = mustExist(useContext(StateContext)); + // eslint-disable-next-line @typescript-eslint/unbound-method + const removeHistory = useStore(state, (s) => s.removeHistory); + const { t } = useTranslation(); + + // TODO: actually retry + const retry = useMutation(() => { + // eslint-disable-next-line no-console + console.log('retry', image); + return Promise.resolve(true); + }); + + return + + + + {t('loading.progress', { + current: ready.progress, + total: image.params.steps, + })} + + + + + + ; +} diff --git a/gui/src/components/tab/Blend.tsx b/gui/src/components/tab/Blend.tsx index a4aed16a..cbeb7d85 100644 --- a/gui/src/components/tab/Blend.tsx +++ b/gui/src/components/tab/Blend.tsx @@ -23,7 +23,7 @@ export function Blend() { sources: mustExist(blend.sources), // TODO: show an error if this doesn't exist }, upscale); - setLoading(output); + pushHistory(output); } const client = mustExist(useContext(ClientContext)); @@ -37,7 +37,7 @@ export function Blend() { // eslint-disable-next-line @typescript-eslint/unbound-method const setBlend = useStore(state, (s) => s.setBlend); // eslint-disable-next-line @typescript-eslint/unbound-method - const setLoading = useStore(state, (s) => s.pushLoading); + const pushHistory = useStore(state, (s) => s.pushHistory); const { t } = useTranslation(); const sources = mustDefault(blend.sources, []); diff --git a/gui/src/components/tab/Img2Img.tsx b/gui/src/components/tab/Img2Img.tsx index 1c5b393c..1d89973e 100644 --- a/gui/src/components/tab/Img2Img.tsx +++ b/gui/src/components/tab/Img2Img.tsx @@ -24,7 +24,7 @@ export function Img2Img() { source: mustExist(img2img.source), // TODO: show an error if this doesn't exist }, upscale); - setLoading(output); + pushHistory(output); } const client = mustExist(useContext(ClientContext)); @@ -39,7 +39,7 @@ export function Img2Img() { // eslint-disable-next-line @typescript-eslint/unbound-method const setImg2Img = useStore(state, (s) => s.setImg2Img); // eslint-disable-next-line @typescript-eslint/unbound-method - const setLoading = useStore(state, (s) => s.pushLoading); + const pushHistory = useStore(state, (s) => s.pushHistory); const { t } = useTranslation(); return diff --git a/gui/src/components/tab/Inpaint.tsx b/gui/src/components/tab/Inpaint.tsx index becb4409..20c935ab 100644 --- a/gui/src/components/tab/Inpaint.tsx +++ b/gui/src/components/tab/Inpaint.tsx @@ -39,7 +39,7 @@ export function Inpaint() { source: mustExist(source), }, upscale); - setLoading(output); + pushHistory(output); } else { const output = await client.inpaint(model, { ...inpaint, @@ -47,7 +47,7 @@ export function Inpaint() { source: mustExist(source), }, upscale); - setLoading(output); + pushHistory(output); } } @@ -72,7 +72,7 @@ export function Inpaint() { // eslint-disable-next-line @typescript-eslint/unbound-method const setInpaint = useStore(state, (s) => s.setInpaint); // eslint-disable-next-line @typescript-eslint/unbound-method - const setLoading = useStore(state, (s) => s.pushLoading); + const pushHistory = useStore(state, (s) => s.pushHistory); const { t } = useTranslation(); const query = useQueryClient(); diff --git a/gui/src/components/tab/Txt2Img.tsx b/gui/src/components/tab/Txt2Img.tsx index 0b735fe2..4990f222 100644 --- a/gui/src/components/tab/Txt2Img.tsx +++ b/gui/src/components/tab/Txt2Img.tsx @@ -18,7 +18,7 @@ export function Txt2Img() { const { model, txt2img, upscale } = state.getState(); const output = await client.txt2img(model, txt2img, upscale); - setLoading(output); + pushHistory(output); } const client = mustExist(useContext(ClientContext)); @@ -33,7 +33,7 @@ export function Txt2Img() { // eslint-disable-next-line @typescript-eslint/unbound-method const setTxt2Img = useStore(state, (s) => s.setTxt2Img); // eslint-disable-next-line @typescript-eslint/unbound-method - const setLoading = useStore(state, (s) => s.pushLoading); + const pushHistory = useStore(state, (s) => s.pushHistory); const { t } = useTranslation(); return diff --git a/gui/src/components/tab/Upscale.tsx b/gui/src/components/tab/Upscale.tsx index 20243f83..9faef6b6 100644 --- a/gui/src/components/tab/Upscale.tsx +++ b/gui/src/components/tab/Upscale.tsx @@ -21,7 +21,7 @@ export function Upscale() { source: mustExist(params.source), // TODO: show an error if this doesn't exist }, upscale); - setLoading(output); + pushHistory(output); } const client = mustExist(useContext(ClientContext)); @@ -35,7 +35,7 @@ export function Upscale() { // eslint-disable-next-line @typescript-eslint/unbound-method const setSource = useStore(state, (s) => s.setUpscaleTab); // eslint-disable-next-line @typescript-eslint/unbound-method - const setLoading = useStore(state, (s) => s.pushLoading); + const pushHistory = useStore(state, (s) => s.pushHistory); const { t } = useTranslation(); return diff --git a/gui/src/state.ts b/gui/src/state.ts index 9c93d93f..2a73670b 100644 --- a/gui/src/state.ts +++ b/gui/src/state.ts @@ -27,7 +27,7 @@ import { Config, ConfigFiles, ConfigState, ServerParams } from './config.js'; */ type TabState = ConfigFiles> & ConfigState>; -interface LoadingItem { +interface HistoryItem { image: ImageResponse; ready: Maybe; } @@ -45,18 +45,12 @@ interface DefaultSlice { } interface HistorySlice { - history: Array; + history: Array; limit: number; - loading: Array; - clearLoading(image: ImageResponse): void; pushHistory(image: ImageResponse): void; - pushLoading(image: ImageResponse): void; removeHistory(image: ImageResponse): void; setLimit(limit: number): void; - /** - * @todo should check ready and move the image from loading to history - */ setReady(image: ImageResponse, ready: ReadyResponse): void; } @@ -164,7 +158,7 @@ export const STATE_KEY = 'onnx-web'; /** * Current state version for zustand persistence. */ -export const STATE_VERSION = 6; +export const STATE_VERSION = 7; export const BLEND_SOURCES = 2; @@ -307,42 +301,27 @@ export function createStateSlices(server: ServerParams) { const createHistorySlice: Slice = (set) => ({ history: [], limit: DEFAULT_HISTORY.limit, - loading: [], - clearLoading(image) { - set((prev) => ({ - ...prev, - loading: prev.loading.filter((it) => it.image.outputs[0].key !== image.outputs[0].key), - })); - }, pushHistory(image) { set((prev) => ({ ...prev, history: [ - image, - ...prev.history, - ].slice(0, prev.limit + DEFAULT_HISTORY.scrollback), - loading: prev.loading.filter((it) => it.image.outputs[0].key !== image.outputs[0].key), - })); - }, - pushLoading(image) { - set((prev) => ({ - ...prev, - loading: [ { image, ready: { + cancelled: false, + failed: false, progress: 0, ready: false, }, }, - ...prev.loading, + ...prev.history, ], })); }, removeHistory(image) { set((prev) => ({ ...prev, - history: prev.history.filter((it) => it.outputs !== image.outputs), + history: prev.history.filter((it) => it.image.outputs[0].key !== image.outputs[0].key), })); }, setLimit(limit) { @@ -353,17 +332,17 @@ export function createStateSlices(server: ServerParams) { }, setReady(image, ready) { set((prev) => { - const loading = [...prev.loading]; - const idx = loading.findIndex((it) => it.image.outputs[0].key === image.outputs[0].key); + const history = [...prev.history]; + const idx = history.findIndex((it) => it.image.outputs[0].key === image.outputs[0].key); if (idx >= 0) { - loading[idx].ready = ready; + history[idx].ready = ready; } else { // TODO: error } return { ...prev, - loading, + history, }; }); },