diff --git a/gui/src/client.ts b/gui/src/client.ts index 5af6dedf..b22c0de5 100644 --- a/gui/src/client.ts +++ b/gui/src/client.ts @@ -141,6 +141,7 @@ export interface ImageResponse { * Status response from the ready endpoint. */ export interface ReadyResponse { + progress: number; ready: boolean; } @@ -213,6 +214,8 @@ export interface ApiClient { * Check whether some pipeline's output is ready yet. */ ready(params: ImageResponse): Promise; + + cancel(params: ImageResponse): Promise; } /** @@ -495,7 +498,14 @@ export function makeClient(root: string, f = fetch): ApiClient { const res = await f(path); return await res.json() as ReadyResponse; - } + }, + async cancel(params: ImageResponse): Promise { + const path = makeApiUrl(root, 'cancel'); + path.searchParams.append('output', params.output.key); + + const res = await f(path); + return res.status === STATUS_SUCCESS; + }, }; } diff --git a/gui/src/components/ImageHistory.tsx b/gui/src/components/ImageHistory.tsx index 946f67b1..3e08bbbc 100644 --- a/gui/src/components/ImageHistory.tsx +++ b/gui/src/components/ImageHistory.tsx @@ -17,12 +17,12 @@ export function ImageHistory() { const children = []; - if (doesExist(loading)) { - children.push(); + if (loading.length > 0) { + children.push(...loading.map((item) => )); } if (history.length > 0) { - children.push(...history.map((item) => )); + children.push(...history.map((item) => )); } else { if (doesExist(loading) === false) { children.push(No results. Press Generate.); diff --git a/gui/src/components/LoadingCard.tsx b/gui/src/components/LoadingCard.tsx index 69cdefac..c04a04c5 100644 --- a/gui/src/components/LoadingCard.tsx +++ b/gui/src/components/LoadingCard.tsx @@ -1,8 +1,8 @@ import { doesExist, mustExist } from '@apextoaster/js-utils'; -import { Card, CardContent, CircularProgress } from '@mui/material'; +import { Button, Card, CardContent, CircularProgress } from '@mui/material'; import * as React from 'react'; import { useContext } from 'react'; -import { useQuery } from 'react-query'; +import { useMutation, useQuery } from 'react-query'; import { useStore } from 'zustand'; import { ImageResponse } from '../client.js'; @@ -17,15 +17,34 @@ export function LoadingCard(props: LoadingCardProps) { 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 pushHistory = useStore(mustExist(useContext(StateContext)), (state) => state.pushHistory); + const clearLoading = useStore(state, (s) => s.clearLoading); + // eslint-disable-next-line @typescript-eslint/unbound-method + const pushHistory = useStore(state, (s) => s.pushHistory); + async function doCancel() { + const cancelled = await client.cancel(props.loading); + if (cancelled) { + clearLoading(); + } + } + + const cancel = useMutation(doCancel); const query = useQuery('ready', () => client.ready(props.loading), { // data will always be ready without this, even if the API says its not cacheTime: 0, refetchInterval: POLL_TIME, }); + function progress() { + if (doesExist(query.data)) { + return Math.ceil(query.data.progress / props.loading.params.steps); + } + + return 0; + } + function ready() { return doesExist(query.data) && query.data.ready; } @@ -44,7 +63,8 @@ export function LoadingCard(props: LoadingCardProps) { justifyContent: 'center', minHeight: params.height.default, }}> - + + ; diff --git a/gui/src/state.ts b/gui/src/state.ts index 4389d9bd..aa1ba1dc 100644 --- a/gui/src/state.ts +++ b/gui/src/state.ts @@ -1,5 +1,5 @@ /* eslint-disable no-null/no-null */ -import { Maybe } from '@apextoaster/js-utils'; +import { doesExist, Maybe } from '@apextoaster/js-utils'; import { createContext } from 'react'; import { StateCreator, StoreApi } from 'zustand'; @@ -12,6 +12,7 @@ import { InpaintParams, ModelParams, OutpaintPixels, + ReadyResponse, Txt2ImgParams, UpscaleParams, UpscaleReqParams, @@ -23,6 +24,11 @@ import { Config, ConfigFiles, ConfigState, ServerParams } from './config.js'; */ type TabState = ConfigFiles> & ConfigState>; +interface LoadingItem { + image: ImageResponse; + ready: Maybe; +} + interface BrushSlice { brush: BrushParams; @@ -38,12 +44,14 @@ interface DefaultSlice { interface HistorySlice { history: Array; limit: number; - loading: Maybe; + loading: Array; + // TODO: hack until setLoading removes things + clearLoading(): void; pushHistory(image: ImageResponse): void; removeHistory(image: ImageResponse): void; setLimit(limit: number): void; - setLoading(image: Maybe): void; + setLoading(image: ImageResponse, ready?: Maybe): void; } interface ModelSlice { @@ -264,7 +272,13 @@ export function createStateSlices(server: ServerParams) { const createHistorySlice: Slice = (set) => ({ history: [], limit: DEFAULT_HISTORY.limit, - loading: null, + loading: [], + clearLoading() { + set((prev) => ({ + ...prev, + loading: [], + })); + }, pushHistory(image) { set((prev) => ({ ...prev, @@ -272,9 +286,25 @@ export function createStateSlices(server: ServerParams) { image, ...prev.history, ].slice(0, prev.limit + DEFAULT_HISTORY.scrollback), - loading: null, + loading: [], })); }, + setLoading(image, ready) { + set((prev) => { + const loading = [...prev.loading]; + const idx = loading.findIndex((it) => it.image.output.key === image.output.key); + if (idx >= 0) { + loading[idx].ready = ready; + } else { + loading.push({ image, ready }); + } + + return { + ...prev, + loading, + }; + }); + }, removeHistory(image) { set((prev) => ({ ...prev, @@ -287,12 +317,6 @@ export function createStateSlices(server: ServerParams) { limit, })); }, - setLoading(loading) { - set((prev) => ({ - ...prev, - loading, - })); - }, }); const createOutpaintSlice: Slice = (set) => ({