1
0
Fork 0

feat(gui): add error status to image card

This commit is contained in:
Sean Sube 2023-03-18 17:34:16 -05:00
parent ed8a7c8934
commit 6226778cfb
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
11 changed files with 124 additions and 85 deletions

View File

@ -175,8 +175,8 @@ export interface ImageResponse {
* Status response from the ready endpoint. * Status response from the ready endpoint.
*/ */
export interface ReadyResponse { export interface ReadyResponse {
cancel: boolean; cancelled: boolean;
error: boolean; failed: boolean;
progress: number; progress: number;
ready: boolean; ready: boolean;
} }

View File

@ -1,38 +1,45 @@
import { doesExist, mustExist } from '@apextoaster/js-utils'; import { doesExist, mustExist } from '@apextoaster/js-utils';
import { Grid, Typography } from '@mui/material'; import { Grid, Typography } from '@mui/material';
import { useContext } from 'react'; import { useContext, ReactNode } from 'react';
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { StateContext } from '../state.js'; import { StateContext } from '../state.js';
import { ImageCard } from './ImageCard.js'; import { ImageCard } from './card/ImageCard.js';
import { LoadingCard } from './LoadingCard.js'; import { LoadingCard } from './card/LoadingCard.js';
import { ErrorCard } from './card/RetryCard.js';
export function ImageHistory() { export function ImageHistory() {
const history = useStore(mustExist(useContext(StateContext)), (state) => state.history); const history = useStore(mustExist(useContext(StateContext)), (state) => state.history);
const limit = useStore(mustExist(useContext(StateContext)), (state) => state.limit); 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 // eslint-disable-next-line @typescript-eslint/unbound-method
const removeHistory = useStore(mustExist(useContext(StateContext)), (state) => state.removeHistory); const removeHistory = useStore(mustExist(useContext(StateContext)), (state) => state.removeHistory);
const { t } = useTranslation(); const { t } = useTranslation();
const children = []; const children: Array<[string, ReactNode]> = [];
if (loading.length > 0) { if (history.length === 0) {
children.push(...loading.map((item) => <LoadingCard key={`loading-${item.image.outputs[0].key}`} index={0} loading={item.image} />)); children.push(['empty', <Typography>{t('history.empty')}</Typography>]);
} }
if (history.length > 0) { const limited = history.slice(0, limit);
children.push(...history.map((item) => <ImageCard key={`history-${item.outputs[0].key}`} value={item} onDelete={removeHistory} />)); for (const item of limited) {
} else { const key = item.image.outputs[0].key;
if (doesExist(loading) === false) {
children.push(<Typography>{t('history.empty')}</Typography>); if (doesExist(item.ready) && item.ready.ready) {
} if (item.ready.cancelled || item.ready.failed) {
children.push([key, <ErrorCard key={`history-${key}`} image={item.image} ready={item.ready} />]);
continue;
} }
const limited = children.slice(0, limit); children.push([key, <ImageCard key={`history-${key}`} image={item.image} onDelete={removeHistory} />]);
continue;
}
return <Grid container spacing={2}>{limited.map((child, idx) => <Grid item key={idx} xs={6}>{child}</Grid>)}</Grid>; children.push([key, <LoadingCard key={`history-${key}`} index={0} image={item.image} />]);
}
return <Grid container spacing={2}>{children.map(([key, child]) => <Grid item key={key} xs={6}>{child}</Grid>)}</Grid>;
} }

View File

@ -7,12 +7,12 @@ import { useTranslation } from 'react-i18next';
import { useHash } from 'react-use/lib/useHash'; import { useHash } from 'react-use/lib/useHash';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { ImageResponse } from '../client/api.js'; import { ImageResponse } from '../../client/api.js';
import { BLEND_SOURCES, ConfigContext, StateContext } from '../state.js'; import { BLEND_SOURCES, ConfigContext, StateContext } from '../../state.js';
import { range, visibleIndex } from '../utils.js'; import { range, visibleIndex } from '../../utils.js';
export interface ImageCardProps { export interface ImageCardProps {
value: ImageResponse; image: ImageResponse;
onDelete?: (key: ImageResponse) => void; onDelete?: (key: ImageResponse) => void;
} }
@ -24,8 +24,8 @@ export function GridItem(props: { xs: number; children: React.ReactNode }) {
} }
export function ImageCard(props: ImageCardProps) { export function ImageCard(props: ImageCardProps) {
const { value } = props; const { image } = props;
const { params, outputs, size } = value; const { params, outputs, size } = image;
const [_hash, setHash] = useHash(); const [_hash, setHash] = useHash();
const [anchor, setAnchor] = useState<Maybe<HTMLElement>>(); const [anchor, setAnchor] = useState<Maybe<HTMLElement>>();
@ -83,7 +83,7 @@ export function ImageCard(props: ImageCardProps) {
function deleteImage() { function deleteImage() {
if (doesExist(props.onDelete)) { if (doesExist(props.onDelete)) {
props.onDelete(value); props.onDelete(image);
} }
} }

View File

@ -7,36 +7,34 @@ import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { ImageResponse } from '../client/api.js'; import { ImageResponse } from '../../client/api.js';
import { POLL_TIME } from '../config.js'; import { POLL_TIME } from '../../config.js';
import { ClientContext, ConfigContext, StateContext } from '../state.js'; import { ClientContext, ConfigContext, StateContext } from '../../state.js';
const LOADING_PERCENT = 100; const LOADING_PERCENT = 100;
const LOADING_OVERAGE = 99; const LOADING_OVERAGE = 99;
export interface LoadingCardProps { export interface LoadingCardProps {
image: ImageResponse;
index: number; index: number;
loading: ImageResponse;
} }
export function LoadingCard(props: LoadingCardProps) { export function LoadingCard(props: LoadingCardProps) {
const { index, loading } = props; const { image, index } = props;
const { steps } = props.loading.params; const { steps } = props.image.params;
const client = mustExist(React.useContext(ClientContext)); const client = mustExist(React.useContext(ClientContext));
const { params } = mustExist(useContext(ConfigContext)); const { params } = mustExist(useContext(ConfigContext));
const state = mustExist(useContext(StateContext)); const state = mustExist(useContext(StateContext));
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const clearLoading = useStore(state, (s) => s.clearLoading); const removeHistory = useStore(state, (s) => s.removeHistory);
// eslint-disable-next-line @typescript-eslint/unbound-method
const pushHistory = useStore(state, (s) => s.pushHistory);
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setReady = useStore(state, (s) => s.setReady); const setReady = useStore(state, (s) => s.setReady);
const { t } = useTranslation(); const { t } = useTranslation();
const cancel = useMutation(() => client.cancel(loading.outputs[index].key)); const cancel = useMutation(() => client.cancel(image.outputs[index].key));
const ready = useQuery(`ready-${loading.outputs[index].key}`, () => client.ready(loading.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 // data will always be ready without this, even if the API says its not
cacheTime: 0, cacheTime: 0,
refetchInterval: POLL_TIME, refetchInterval: POLL_TIME,
@ -86,17 +84,13 @@ export function LoadingCard(props: LoadingCardProps) {
useEffect(() => { useEffect(() => {
if (cancel.status === 'success') { if (cancel.status === 'success') {
clearLoading(props.loading); removeHistory(props.image);
} }
}, [cancel.status]); }, [cancel.status]);
useEffect(() => { useEffect(() => {
if (ready.status === 'success') { if (ready.status === 'success' && getReady()) {
if (ready.data.ready) { setReady(props.image, ready.data);
pushHistory(props.loading);
} else {
setReady(props.loading, ready.data);
}
} }
}, [ready.status, getReady(), getProgress()]); }, [ready.status, getReady(), getProgress()]);

View File

@ -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 <Card sx={{ maxWidth: params.width.default }}>
<CardContent sx={{ height: params.height.default }}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: params.height.default,
}}>
<Stack
direction='column'
spacing={2}
sx={{ alignItems: 'center' }}
>
<Typography>{t('loading.progress', {
current: ready.progress,
total: image.params.steps,
})}</Typography>
<Button onClick={() => retry.mutate()}>{t('loading.retry')}</Button>
<Button onClick={() => removeHistory(image)}>{t('loading.remove')}</Button>
</Stack>
</Box>
</CardContent>
</Card>;
}

View File

@ -23,7 +23,7 @@ export function Blend() {
sources: mustExist(blend.sources), // TODO: show an error if this doesn't exist sources: mustExist(blend.sources), // TODO: show an error if this doesn't exist
}, upscale); }, upscale);
setLoading(output); pushHistory(output);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));
@ -37,7 +37,7 @@ export function Blend() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setBlend = useStore(state, (s) => s.setBlend); const setBlend = useStore(state, (s) => s.setBlend);
// eslint-disable-next-line @typescript-eslint/unbound-method // 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 { t } = useTranslation();
const sources = mustDefault(blend.sources, []); const sources = mustDefault(blend.sources, []);

View File

@ -24,7 +24,7 @@ export function Img2Img() {
source: mustExist(img2img.source), // TODO: show an error if this doesn't exist source: mustExist(img2img.source), // TODO: show an error if this doesn't exist
}, upscale); }, upscale);
setLoading(output); pushHistory(output);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));
@ -39,7 +39,7 @@ export function Img2Img() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setImg2Img = useStore(state, (s) => s.setImg2Img); const setImg2Img = useStore(state, (s) => s.setImg2Img);
// eslint-disable-next-line @typescript-eslint/unbound-method // 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 { t } = useTranslation();
return <Box> return <Box>

View File

@ -39,7 +39,7 @@ export function Inpaint() {
source: mustExist(source), source: mustExist(source),
}, upscale); }, upscale);
setLoading(output); pushHistory(output);
} else { } else {
const output = await client.inpaint(model, { const output = await client.inpaint(model, {
...inpaint, ...inpaint,
@ -47,7 +47,7 @@ export function Inpaint() {
source: mustExist(source), source: mustExist(source),
}, upscale); }, upscale);
setLoading(output); pushHistory(output);
} }
} }
@ -72,7 +72,7 @@ export function Inpaint() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setInpaint = useStore(state, (s) => s.setInpaint); const setInpaint = useStore(state, (s) => s.setInpaint);
// eslint-disable-next-line @typescript-eslint/unbound-method // 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 { t } = useTranslation();
const query = useQueryClient(); const query = useQueryClient();

View File

@ -18,7 +18,7 @@ export function Txt2Img() {
const { model, txt2img, upscale } = state.getState(); const { model, txt2img, upscale } = state.getState();
const output = await client.txt2img(model, txt2img, upscale); const output = await client.txt2img(model, txt2img, upscale);
setLoading(output); pushHistory(output);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));
@ -33,7 +33,7 @@ export function Txt2Img() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setTxt2Img = useStore(state, (s) => s.setTxt2Img); const setTxt2Img = useStore(state, (s) => s.setTxt2Img);
// eslint-disable-next-line @typescript-eslint/unbound-method // 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 { t } = useTranslation();
return <Box> return <Box>

View File

@ -21,7 +21,7 @@ export function Upscale() {
source: mustExist(params.source), // TODO: show an error if this doesn't exist source: mustExist(params.source), // TODO: show an error if this doesn't exist
}, upscale); }, upscale);
setLoading(output); pushHistory(output);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));
@ -35,7 +35,7 @@ export function Upscale() {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const setSource = useStore(state, (s) => s.setUpscaleTab); const setSource = useStore(state, (s) => s.setUpscaleTab);
// eslint-disable-next-line @typescript-eslint/unbound-method // 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 { t } = useTranslation();
return <Box> return <Box>

View File

@ -27,7 +27,7 @@ import { Config, ConfigFiles, ConfigState, ServerParams } from './config.js';
*/ */
type TabState<TabParams> = ConfigFiles<Required<TabParams>> & ConfigState<Required<TabParams>>; type TabState<TabParams> = ConfigFiles<Required<TabParams>> & ConfigState<Required<TabParams>>;
interface LoadingItem { interface HistoryItem {
image: ImageResponse; image: ImageResponse;
ready: Maybe<ReadyResponse>; ready: Maybe<ReadyResponse>;
} }
@ -45,18 +45,12 @@ interface DefaultSlice {
} }
interface HistorySlice { interface HistorySlice {
history: Array<ImageResponse>; history: Array<HistoryItem>;
limit: number; limit: number;
loading: Array<LoadingItem>;
clearLoading(image: ImageResponse): void;
pushHistory(image: ImageResponse): void; pushHistory(image: ImageResponse): void;
pushLoading(image: ImageResponse): void;
removeHistory(image: ImageResponse): void; removeHistory(image: ImageResponse): void;
setLimit(limit: number): void; setLimit(limit: number): void;
/**
* @todo should check ready and move the image from loading to history
*/
setReady(image: ImageResponse, ready: ReadyResponse): void; setReady(image: ImageResponse, ready: ReadyResponse): void;
} }
@ -164,7 +158,7 @@ export const STATE_KEY = 'onnx-web';
/** /**
* Current state version for zustand persistence. * Current state version for zustand persistence.
*/ */
export const STATE_VERSION = 6; export const STATE_VERSION = 7;
export const BLEND_SOURCES = 2; export const BLEND_SOURCES = 2;
@ -307,42 +301,27 @@ export function createStateSlices(server: ServerParams) {
const createHistorySlice: Slice<HistorySlice> = (set) => ({ const createHistorySlice: Slice<HistorySlice> = (set) => ({
history: [], history: [],
limit: DEFAULT_HISTORY.limit, 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) { pushHistory(image) {
set((prev) => ({ set((prev) => ({
...prev, ...prev,
history: [ 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, image,
ready: { ready: {
cancelled: false,
failed: false,
progress: 0, progress: 0,
ready: false, ready: false,
}, },
}, },
...prev.loading, ...prev.history,
], ],
})); }));
}, },
removeHistory(image) { removeHistory(image) {
set((prev) => ({ set((prev) => ({
...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) { setLimit(limit) {
@ -353,17 +332,17 @@ export function createStateSlices(server: ServerParams) {
}, },
setReady(image, ready) { setReady(image, ready) {
set((prev) => { set((prev) => {
const loading = [...prev.loading]; const history = [...prev.history];
const idx = loading.findIndex((it) => it.image.outputs[0].key === image.outputs[0].key); const idx = history.findIndex((it) => it.image.outputs[0].key === image.outputs[0].key);
if (idx >= 0) { if (idx >= 0) {
loading[idx].ready = ready; history[idx].ready = ready;
} else { } else {
// TODO: error // TODO: error
} }
return { return {
...prev, ...prev,
loading, history,
}; };
}); });
}, },