Refactor UI queue state

This commit is contained in:
space-nuko
2023-05-31 17:34:14 -05:00
parent 263d62cb34
commit 3cd623fd20
4 changed files with 252 additions and 126 deletions

View File

@@ -30,6 +30,7 @@
import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte"; import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte";
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte"; import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte"; import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
import uiQueueState from "$lib/stores/uiQueueState";
export let app: ComfyApp; export let app: ComfyApp;
@@ -52,46 +53,34 @@
let displayMode: DisplayModeType = "list"; let displayMode: DisplayModeType = "list";
let imageSize: number = 40; let imageSize: number = 40;
let gridColumns: number = 3; let gridColumns: number = 3;
let changed = true;
function switchMode(newMode: QueueItemType) { function switchMode(newMode: QueueItemType) {
changed = mode !== newMode const changed = mode !== newMode
mode = newMode mode = newMode
if (changed) { if (changed) {
_queuedEntries = [] uiQueueState.updateEntries();
_runningEntries = []
_entries = []
} }
} }
function switchDisplayMode(newDisplayMode: DisplayModeType) { function switchDisplayMode(newDisplayMode: DisplayModeType) {
// changed = displayMode !== newDisplayMode
displayMode = newDisplayMode displayMode = newDisplayMode
// if (changed) {
// _queuedEntries = []
// _runningEntries = []
// _entries = []
// }
} }
let _queuedEntries: QueueUIEntry[] = [] let _entries: ReadonlyArray<QueueUIEntry> = []
let _runningEntries: QueueUIEntry[] = [] $: if(mode === "queue") {
let _entries: QueueUIEntry[] = [] _entries = $uiQueueState.queueUIEntries
$: if (mode === "queue" && (changed || $queuePending.length != _queuedEntries.length || $queueRunning.length != _runningEntries.length)) {
updateFromQueue(); updateFromQueue();
changed = false;
} }
else if (mode === "history" && (changed || $queueCompleted.length != _entries.length)) { else {
_entries = $uiQueueState.historyUIEntries;
updateFromHistory(); updateFromHistory();
changed = false;
} }
$: if (mode === "queue" && !$queuePending && !$queueRunning) { $: if (mode === "queue" && !$queuePending && !$queueRunning) {
_queuedEntries = [] uiQueueState.clearQueue();
_runningEntries = [] }
_entries = []; else if (mode === "history" && !$queueCompleted) {
changed = true uiQueueState.clearHistory();
} }
async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) { async function deleteEntry(entry: QueueUIEntry, event: MouseEvent) {
@@ -106,125 +95,26 @@
await app.deleteQueueItem(mode, entry.entry.promptID); await app.deleteQueueItem(mode, entry.entry.promptID);
} }
if (mode === "queue") { uiQueueState.updateEntries(true)
_queuedEntries = []
_runningEntries = []
}
_entries = [];
changed = true;
} }
async function clearQueue() { async function clearQueue() {
await app.clearQueue(mode); await app.clearQueue(mode);
uiQueueState.updateEntries(true)
if (mode === "queue") {
_queuedEntries = []
_runningEntries = []
}
_entries = [];
changed = true;
}
function formatDate(date: Date): string {
const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', '');
return [time, day].join(", ")
}
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
let date = entry.finishedAt || entry.queuedAt;
let dateStr = null;
if (date) {
dateStr = formatDate(date);
}
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
let message = "Prompt";
if (entry.extraData?.workflowTitle != null) {
message = `${entry.extraData.workflowTitle}`
}
if (subgraphs && subgraphs.length > 0) {
const subgraphsString = subgraphs.join(', ')
message += ` (${subgraphsString})`
}
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
if (Object.keys(entry.outputs).length > 0) {
const imageCount = Object.values(entry.outputs).filter(o => o.images).flatMap(o => o.images).length
submessage = `Images: ${imageCount}`
}
return {
entry,
message,
submessage,
date: dateStr,
status,
images: []
}
}
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
const result = convertEntry(entry, status);
const thumbnails = entry.extraData?.thumbnails
if (thumbnails) {
result.images = thumbnails.map(convertComfyOutputToComfyURL);
}
const outputs = Object.values(entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
if (outputs) {
result.images = result.images.concat(outputs)
}
return result;
}
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
if (entry.message)
result.submessage = entry.message
else if (entry.status === "interrupted" || entry.status === "all_cached")
result.submessage = "Prompt was interrupted."
if (entry.error)
result.error = entry.error
return result;
} }
async function updateFromQueue() { async function updateFromQueue() {
// newest entries appear at the top
_queuedEntries = $queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
_runningEntries = $queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
_entries = [..._queuedEntries, ..._runningEntries]
if (queueList) { if (queueList) {
await tick(); // Wait for list size to be recalculated await tick(); // Wait for list size to be recalculated
queueList.scroll({ top: queueList.scrollHeight }) queueList.scroll({ top: queueList.scrollHeight })
} }
console.warn("[ComfyQueue] BUILDQUEUE", _entries.length, $queuePending.length, $queueRunning.length)
} }
async function updateFromHistory() { async function updateFromHistory() {
_entries = $queueCompleted.map(convertCompletedEntry).reverse();
if (queueList) { if (queueList) {
await tick(); // Wait for list size to be recalculated
queueList.scrollTo(0, 0); queueList.scrollTo(0, 0);
} }
console.warn("[ComfyQueue] BUILDHISTORY", _entries.length, $queueCompleted.length)
} }
async function interrupt() { async function interrupt() {

View File

@@ -8,6 +8,7 @@ import { get, writable, type Writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState"; import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState";
import configState from "./configState"; import configState from "./configState";
import uiQueueState from "./uiQueueState";
export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown"; export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown";

View File

@@ -0,0 +1,207 @@
import type { PromptID, QueueItemType } from '$lib/api';
import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import queueState, { type CompletedQueueEntry, type QueueEntry } from './queueState';
import type { WorkflowError } from './workflowState';
import { convertComfyOutputToComfyURL } from '$lib/utils';
export type QueueUIEntryStatus = QueueEntryStatus | "pending" | "running";
export type QueueUIEntry = {
entry: QueueEntry,
message: string,
submessage: string,
date?: string,
status: QueueUIEntryStatus,
images?: string[], // URLs
details?: string, // shown in a tooltip on hover
error?: WorkflowError
}
export type UIQueueState = {
mode: QueueItemType,
queuedEntries: QueueUIEntry[],
runningEntries: QueueUIEntry[],
queueUIEntries: QueueUIEntry[],
historyUIEntries: QueueUIEntry[],
}
type UIQueueStateOps = {
updateEntries: (force?: boolean) => void
clearAll: () => void
clearQueue: () => void
clearHistory: () => void
}
export type WritableUIQueueStateStore = Writable<UIQueueState> & UIQueueStateOps;
const store: Writable<UIQueueState> = writable(
{
mode: "queue",
queuedEntries: [],
runningEntries: [],
completedEntries: [],
queueUIEntries: [],
historyUIEntries: [],
})
function formatDate(date: Date): string {
const time = date.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true });
const day = date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }).replace(',', '');
return [time, day].join(", ")
}
function convertEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
let date = entry.finishedAt || entry.queuedAt;
let dateStr = null;
if (date) {
dateStr = formatDate(date);
}
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxPrompt?.subgraphs;
let message = "Prompt";
if (entry.extraData?.workflowTitle != null) {
message = `${entry.extraData.workflowTitle}`
}
if (subgraphs && subgraphs.length > 0) {
const subgraphsString = subgraphs.join(', ')
message += ` (${subgraphsString})`
}
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
if (Object.keys(entry.outputs).length > 0) {
const imageCount = Object.values(entry.outputs).filter(o => o.images).flatMap(o => o.images).length
submessage = `Images: ${imageCount}`
}
return {
entry,
message,
submessage,
date: dateStr,
status,
images: []
}
}
function convertPendingEntry(entry: QueueEntry, status: QueueUIEntryStatus): QueueUIEntry {
const result = convertEntry(entry, status);
const thumbnails = entry.extraData?.thumbnails
if (thumbnails) {
result.images = thumbnails.map(convertComfyOutputToComfyURL);
}
const outputs = Object.values(entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
if (outputs) {
result.images = result.images.concat(outputs)
}
return result;
}
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
const result = convertEntry(entry.entry, entry.status);
const images = Object.values(entry.entry.outputs)
.filter(o => o.images)
.flatMap(o => o.images)
.map(convertComfyOutputToComfyURL);
result.images = images
if (entry.message)
result.submessage = entry.message
else if (entry.status === "interrupted" || entry.status === "all_cached")
result.submessage = "Prompt was interrupted."
if (entry.error)
result.error = entry.error
return result;
}
function updateFromQueue(queuePending: QueueEntry[], queueRunning: QueueEntry[]) {
store.update(s => {
// newest entries appear at the top
s.queuedEntries = queuePending.map((e) => convertPendingEntry(e, "pending")).reverse();
s.runningEntries = queueRunning.map((e) => convertPendingEntry(e, "running")).reverse();
s.queueUIEntries = s.queuedEntries.concat(s.runningEntries);
console.warn("[ComfyQueue] BUILDQUEUE", s.queuedEntries.length, s.runningEntries.length)
return s;
})
}
function updateFromHistory(queueCompleted: CompletedQueueEntry[]) {
store.update(s => {
s.historyUIEntries = queueCompleted.map(convertCompletedEntry).reverse();
console.warn("[ComfyQueue] BUILDHISTORY", s.historyUIEntries.length)
return s
})
}
function updateEntries(force: boolean = false) {
const state = get(store)
const qs = get(queueState)
const queuePending = qs.queuePending
const queueRunning = qs.queueRunning
const queueCompleted = qs.queueCompleted
const queueChanged = get(queuePending).length != state.queuedEntries.length
|| get(queueRunning).length != state.runningEntries.length;
const historyChanged = get(queueCompleted).length != state.historyUIEntries.length;
if (queueChanged || force) {
updateFromQueue(get(queuePending), get(queueRunning));
}
if (historyChanged || force) {
updateFromHistory(get(queueCompleted));
}
}
function clearAll() {
store.update(s => {
s.queuedEntries = []
s.runningEntries = []
s.historyUIEntries = []
return s
})
updateEntries(true);
}
function clearQueue() {
store.update(s => {
s.queuedEntries = []
s.runningEntries = []
return s
})
updateEntries(true);
}
function clearHistory() {
store.update(s => {
s.historyUIEntries = []
return s
})
updateEntries(true);
}
queueState.subscribe(s => {
updateEntries();
})
const uiStateStore: WritableUIQueueStateStore =
{
...store,
updateEntries,
clearAll,
clearQueue,
clearHistory,
}
export default uiStateStore;

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Page, Navbar, Tabs, Tab, NavLeft, NavTitle, NavRight, Link } from "framework7-svelte" import { Page, Navbar, Block, Tabs, Tab, NavLeft, NavTitle, NavRight, Link } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte"; import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp"; import type ComfyApp from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
@@ -12,6 +12,34 @@
<Page name="gallery"> <Page name="gallery">
<Navbar title="Gallery" /> <Navbar title="Gallery" />
<Block>
<div class="grid grid-cols-3 grid-gap">
<div>3 cols</div>
<div>3 cols</div>
<div>3 cols</div>
</div>
<div class="grid grid-cols-3 grid-gap">
<div>3 cols</div>
<div>3 cols</div>
<div>3 cols</div>
</div>
<div class="grid grid-cols-3 grid-gap">
<div>3 cols</div>
<div>3 cols</div>
<div>3 cols</div>
</div>
<div class="grid grid-cols-3 grid-gap">
<div>3 cols</div>
<div>3 cols</div>
<div>3 cols</div>
</div>
<div class="grid grid-cols-3 grid-gap">
<div>3 cols</div>
<div>3 cols</div>
<div>3 cols</div>
</div>
</Block>
</Page> </Page>
<style lang="scss"> <style lang="scss">