Files
ComfyBox/src/lib/components/ComfyQueue.svelte
space-nuko 4a92bb68ee fixes
2023-06-08 19:33:26 -05:00

403 lines
11 KiB
Svelte

<script lang="ts">
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
import ProgressBar from "./ProgressBar.svelte";
import SystemStatsBar from "./SystemStatsBar.svelte";
import Spinner from "./Spinner.svelte";
import PromptDisplay from "./PromptDisplay.svelte";
import { List, ListUl, Grid } from "svelte-bootstrap-icons";
import { getNodeInfo, type ComfyImageLocation } from "$lib/utils"
import type { Writable } from "svelte/store";
import type { QueueItemType } from "$lib/api";
import { Button } from "@gradio/button";
import type ComfyApp from "./ComfyApp";
import { getContext, tick } from "svelte";
import Modal from "./Modal.svelte";
import { type WorkflowError } from "$lib/stores/workflowState";
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
import uiQueueState, { type QueueUIEntry } from "$lib/stores/uiQueueState";
export let app: ComfyApp;
let queuePending: Writable<QueueEntry[]> | null = null;
let queueRunning: Writable<QueueEntry[]> | null = null;
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
let queueList: HTMLDivElement | null = null;
const { showError } = getContext(WORKFLOWS_VIEW) as any;
$: if ($queueState) {
queuePending = $queueState.queuePending
queueRunning = $queueState.queueRunning
queueCompleted = $queueState.queueCompleted
}
type DisplayModeType = "list" | "grid";
let mode: QueueItemType = "queue";
let displayMode: DisplayModeType = "list";
let imageSize: number = 40;
let gridColumns: number = 3;
function switchMode(newMode: QueueItemType) {
const changed = mode !== newMode
mode = newMode
if (changed) {
uiQueueState.updateEntries();
}
}
function switchDisplayMode(newDisplayMode: DisplayModeType) {
displayMode = newDisplayMode
}
let _entries: ReadonlyArray<QueueUIEntry> = []
$: if(mode === "queue") {
_entries = $uiQueueState.queueUIEntries
updateFromQueue();
}
else {
_entries = $uiQueueState.historyUIEntries;
updateFromHistory();
}
$: if (mode === "queue" && !$queuePending && !$queueRunning) {
uiQueueState.clearQueue();
}
else if (mode === "history" && !$queueCompleted) {
uiQueueState.clearHistory();
}
async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) {
event.preventDefault();
event.stopImmediatePropagation()
// TODO support interrupting from multiple running items!
if (entry.status === "running") {
await app.interrupt();
}
else {
await app.deleteQueueItem(mode, entry.entry.promptID);
}
uiQueueState.updateEntries(true)
}
async function clearQueue() {
await app.clearQueue(mode);
uiQueueState.updateEntries(true)
}
async function updateFromQueue() {
if (queueList) {
await tick(); // Wait for list size to be recalculated
queueList.scroll({ top: queueList.scrollHeight })
}
}
async function updateFromHistory() {
if (queueList) {
await tick(); // Wait for list size to be recalculated
queueList.scrollTo(0, 0);
}
}
async function interrupt() {
await app.interrupt();
}
let showModal = false;
let expandAll = false;
let selectedPrompt = null;
let selectedImages: ComfyImageLocation[] = [];
function showPrompt(entry: QueueUIEntry) {
if (entry.error != null) {
showModal = false;
expandAll = false;
selectedPrompt = null;
selectedImages = [];
showError(entry.entry.promptID);
}
else {
selectedPrompt = entry.entry.prompt;
selectedImages = entry.images;
showModal = true;
expandAll = false
}
}
function closeModal() {
selectedPrompt = null
selectedImages = []
showModal = false;
expandAll = false;
console.warn("CLOSEMODAL")
}
let queued = false
$: queued = Boolean($queueState.runningNodeID || $queueState.progress);
let inProgress = false;
$: inProgress = typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0;
</script>
<Modal bind:showModal>
<div slot="header" class="prompt-modal-header">
<h1 style="padding-bottom: 1rem;">Prompt Details</h1>
</div>
<svelte:fragment let:closeDialog>
{#if selectedPrompt}
<PromptDisplay closeModal={() => { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} images={selectedImages} {expandAll} />
{/if}
</svelte:fragment>
<div slot="buttons" let:closeDialog>
<Button variant="secondary" on:click={closeDialog}>
Close
</Button>
<Button variant="secondary" on:click={() => (expandAll = !expandAll)}>
Expand All
</Button>
</div>
</Modal>
<div class="queue {mode}-mode">
{#if mode === "history"}
<div class="display-mode-buttons">
<div class="mode-button image-display-button ternary"
on:click={() => switchDisplayMode("list")}
class:selected={displayMode === "list"}>
<List width="100%" height="100%" />
</div>
<div class="mode-button image-display-button ternary"
on:click={() => switchDisplayMode("grid")}
class:selected={displayMode === "grid"}>
<Grid width="100%" height="100%" />
</div>
</div>
{/if}
<div class="queue-entries" bind:this={queueList}>
{#if _entries.length > 0}
{#if mode === "history" && displayMode === "grid"}
<ComfyQueueGridDisplay entries={_entries} {showPrompt} {clearQueue} {mode} bind:gridColumns />
{:else}
<ComfyQueueListDisplay entries={_entries} {showPrompt} {clearQueue} {mode} {deleteEntry} bind:imageSize />
{/if}
{:else}
<div class="queue-empty">
<div class="queue-empty-container">
<div class="queue-empty-icon">
<ListUl width="100%" height="10rem" />
</div>
<div class="queue-empty-message">
(No entries)
</div>
</div>
</div>
{/if}
</div>
<div class="mode-buttons">
<div class="mode-button secondary"
on:click={() => switchMode("queue")}
class:selected={mode === "queue"}>
Queue
</div>
<div class="mode-button secondary"
on:click={() => switchMode("history")}
class:selected={mode === "history"}>
History
</div>
</div>
<div class="bottom">
<div class="queue-remaining" class:queued class:in-progress={inProgress}>
{#if inProgress}
<Spinner />
<div class="status">
Queued prompts: {$queueState.queueRemaining}
</div>
{:else}
<div>
Nothing queued.
</div>
{/if}
</div>
{#if queued}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeID)}</span>
</div>
<div>
<SystemStatsBar />
</div>
<div>
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
</div>
<div class="queue-action-buttons">
<Button variant="secondary"
disabled={$queueState.isInterrupting}
on:click={interrupt}
style={{ full_width: true }}>
Interrupt
</Button>
</div>
{/if}
</div>
</div>
<style lang="scss">
$pending-height: 200px;
$display-mode-buttons-height: 2rem;
$pane-mode-buttons-height: 2.5rem;
$bottom-bar-height: 70px;
$workflow-tabs-height: 2.5rem;
$mode-buttons-height: 30px;
$system-stats-bar-height: 24px;
$queue-height: calc(100vh - #{$pending-height} - #{$pane-mode-buttons-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem - #{$system-stats-bar-height});
$queue-height-history: calc(#{$queue-height} - #{$display-mode-buttons-height});
.prompt-modal-header {
padding-left: 0.2rem;
h1 {
font-size: large;
}
}
.queue {
color: var(--body-text-color);
&.queue-mode > .queue-entries {
height: $queue-height;
max-height: $queue-height;
}
&.history-mode > .queue-entries {
height: $queue-height-history;
max-height: $queue-height-history;
}
}
.queue-entries {
display: flex;
flex-flow: column nowrap;
height: $queue-height;
> .queue-empty {
display: flex;
color: var(--comfy-accent-soft);
flex-direction: row;
margin: auto;
height: 100%;
> .queue-empty-container {
margin: auto;
display: flex;
flex-direction: column;
> .queue-empty-icon {
margin: auto;
}
> .queue-empty-message {
margin: auto;
font-size: 32px;
font-weight: bolder;
}
}
}
}
.display-mode-buttons {
display: flex;
flex-direction: row;
top: 0px;
height: $display-mode-buttons-height;
margin-bottom: auto;
> .mode-button {
width: 100%;
color: var(--neutral-500);
&.selected {
color: var(--body-text-color);
}
}
}
.mode-buttons {
display: flex;
flex-direction: row;
justify-content: center;
height: 100%;
> .mode-button {
width: 100%;
}
}
.mode-button {
height: calc($mode-buttons-height);
padding: 0.2rem;
@include square-button;
}
:global(.dark) .mode-button {
filter: none;
&:hover {
filter: brightness(120%);
}
&:active {
filter: brightness(50%)
}
&.selected {
filter: brightness(150%)
}
}
.bottom {
width: 100%;
height: calc($pending-height);
position: absolute;
.node-name {
background-color: var(--comfy-node-name-background);
color: var(--comfy-node-name-foreground);
padding: 0.2em;
margin: 5px;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.queue-remaining {
height: calc($pending-height - $bottom-bar-height - 50px);
width: 100%;
text-align: center;
position: relative;
display: flex;
justify-content: space-evenly;
align-items: center;
background: var(--panel-background-fill);
> .status {
}
&.queued {
height: calc($pending-height - $mode-buttons-height - $bottom-bar-height - 16px);
}
}
.queue-action-buttons {
margin: 5px;
height: 20px;
:global(button) {
border-radius: 0px !important;
}
}
}
</style>