Queue/history bar
This commit is contained in:
@@ -78,7 +78,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let propsSidebarSize = 15; //15;
|
||||
let propsSidebarSize = 0;
|
||||
|
||||
function toggleProps() {
|
||||
if (propsSidebarSize == 0) {
|
||||
@@ -90,11 +90,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
let queueSidebarSize = 15;
|
||||
let queueSidebarSize = 20;
|
||||
|
||||
function toggleQueue() {
|
||||
if (queueSidebarSize == 0) {
|
||||
queueSidebarSize = 15;
|
||||
queueSidebarSize = 20;
|
||||
app.resizeCanvas();
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot } from "@litegraph-ts/core";
|
||||
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||
import ComfyAPI, { type ComfyAPIStatusResponse, type NodeID, type PromptID } from "$lib/api"
|
||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyPromptExtraData, type ComfyPromptRequest, type NodeID, type PromptID } from "$lib/api"
|
||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
@@ -385,6 +385,7 @@ export default class ComfyApp {
|
||||
const queue = await this.api.getQueue();
|
||||
const history = await this.api.getHistory();
|
||||
queueState.queueUpdated(queue);
|
||||
queueState.historyUpdated(history);
|
||||
}
|
||||
|
||||
private requestPermissions() {
|
||||
@@ -739,13 +740,24 @@ export default class ComfyApp {
|
||||
const p = await this.graphToPrompt(tag);
|
||||
console.debug(promptToGraphVis(p))
|
||||
|
||||
const extra_data = { extra_pnginfo: { workflow: p.workflow } }
|
||||
const extraData: ComfyPromptExtraData = {
|
||||
extra_pnginfo: {
|
||||
workflow: p.workflow
|
||||
},
|
||||
subgraphs: [tag]
|
||||
}
|
||||
|
||||
let error = null;
|
||||
let promptID = null;
|
||||
|
||||
const request: ComfyPromptRequest = {
|
||||
number: num,
|
||||
extra_data: extraData,
|
||||
prompt: p.output
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.api.queuePrompt(num, p, extra_data);
|
||||
const response = await this.api.queuePrompt(request);
|
||||
promptID = response.promptID;
|
||||
error = response.error;
|
||||
} catch (error) {
|
||||
@@ -768,7 +780,7 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
this.lCanvas.draw(true, true);
|
||||
queueState.afterQueued(promptID, num, p, extra_data)
|
||||
queueState.afterQueued(promptID, num, p.output, extraData)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,74 +1,113 @@
|
||||
<script lang="ts">
|
||||
import queueState, { type QueueEntry } from "$lib/stores/queueState";
|
||||
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
import { getNodeInfo } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import Spinner from "./Spinner.svelte";
|
||||
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { QueueItemType } from "$lib/api";
|
||||
|
||||
let queuePending: Writable<QueueEntry[]> | null = null;
|
||||
let queueRunning: Writable<QueueEntry[]> | null = null;
|
||||
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
|
||||
|
||||
type QueueUIEntry = {
|
||||
message: string,
|
||||
submessage: string,
|
||||
date?: string,
|
||||
status: "success" | "error" | "pending" | "running" | "all_cached",
|
||||
images?: string[] // URLs
|
||||
status: QueueEntryStatus | "pending" | "running",
|
||||
images?: string[], // URLs
|
||||
error?: string
|
||||
}
|
||||
|
||||
$: if ($queueState) {
|
||||
queuePending = $queueState.queuePending
|
||||
queueRunning = $queueState.queueRunning
|
||||
queueCompleted = $queueState.queueCompleted
|
||||
}
|
||||
|
||||
let mode: QueueItemType = "queue";
|
||||
|
||||
function switchMode(newMode: QueueItemType) {
|
||||
console.warn("SwitchMode", newMode)
|
||||
const changed = mode !== newMode
|
||||
mode = newMode
|
||||
if (changed)
|
||||
_entries = []
|
||||
}
|
||||
|
||||
let _entries: QueueUIEntry[] = []
|
||||
|
||||
$: if ($queuePending && $queuePending.length != _entries.length) {
|
||||
_entries = []
|
||||
for (const entry of $queuePending) {
|
||||
// for (const outputs of Object.values(entry.outputs)) {
|
||||
// const allImages = outputs.images.map(r => {
|
||||
// // TODO configure backend URL
|
||||
// const url = "http://localhost:8188/view?"
|
||||
// const params = new URLSearchParams(r)
|
||||
// return url + params
|
||||
// });
|
||||
//
|
||||
// _entries.push({ allImages, name: "Output" })
|
||||
// }
|
||||
|
||||
let date = null;
|
||||
if (entry.queuedAt) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
};
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-US', options);
|
||||
date = dateTimeFormat.format(entry.queuedAt);
|
||||
}
|
||||
|
||||
_entries.push({
|
||||
message: "Prompt",
|
||||
submessage: ".......",
|
||||
date,
|
||||
status: "pending",
|
||||
images: null
|
||||
})
|
||||
}
|
||||
console.error("BUILDENTRIES", _entries, $queuePending)
|
||||
$: if (mode === "queue" && $queuePending && $queuePending.length != _entries.length) {
|
||||
updateFromQueue();
|
||||
}
|
||||
else if (mode === "history" && $queueCompleted && $queueCompleted.length != _entries.length) {
|
||||
updateFromHistory();
|
||||
}
|
||||
|
||||
function convertEntry(entry: QueueEntry): QueueUIEntry {
|
||||
const images = Object.values(entry.outputs).flatMap(o => o.images)
|
||||
.map(convertComfyOutputToComfyURL);
|
||||
|
||||
let date = null;
|
||||
if (entry.queuedAt) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
};
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-US', options);
|
||||
date = dateTimeFormat.format(entry.queuedAt);
|
||||
}
|
||||
|
||||
let message = "Prompt";
|
||||
if (entry.extraData.subgraphs)
|
||||
message = `Prompt: ${entry.extraData.subgraphs.join(', ')}`
|
||||
|
||||
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||
|
||||
return {
|
||||
message,
|
||||
submessage,
|
||||
date,
|
||||
status: "pending",
|
||||
images
|
||||
}
|
||||
}
|
||||
|
||||
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
||||
const result = convertEntry(entry.entry);
|
||||
result.status = entry.status;
|
||||
result.error = entry.error;
|
||||
return result;
|
||||
}
|
||||
|
||||
function updateFromQueue() {
|
||||
_entries = $queuePending.map(convertEntry);
|
||||
console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending)
|
||||
}
|
||||
|
||||
function updateFromHistory() {
|
||||
_entries = $queueCompleted.map(convertCompletedEntry);
|
||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
|
||||
}
|
||||
|
||||
let queued = false
|
||||
$: queued = Boolean($queueState.runningNodeID || $queueState.progress);
|
||||
|
||||
let inProgress = false;
|
||||
$: inProgress = typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0;
|
||||
</script>
|
||||
|
||||
<div class="queue">
|
||||
<div class="queue-entries">
|
||||
{#each _entries as entry}
|
||||
<div class="queue-entry {entry.status}">
|
||||
{#if entry.images}
|
||||
{#if entry.images.length > 0}
|
||||
<img class="queue-entry-image" src={entry.images[0]} alt="thumbnail" />
|
||||
{:else}
|
||||
<div class="queue-entry-image-placeholder" />
|
||||
<!-- <div class="queue-entry-image-placeholder" /> -->
|
||||
{/if}
|
||||
<div class="queue-entry-details">
|
||||
<div class="queue-entry-message">
|
||||
@@ -88,68 +127,79 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mode-buttons">
|
||||
<div class="mode-button"
|
||||
on:click={() => switchMode("queue")}
|
||||
class:mode-selected={mode === "queue"}>
|
||||
Queue
|
||||
</div>
|
||||
<div class="mode-button"
|
||||
on:click={() => switchMode("history")}
|
||||
class:mode-selected={mode === "history"}>
|
||||
History
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
{#if $queueState.runningNodeID || $queueState.progress}
|
||||
<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>
|
||||
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} styles="height: 30px;" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}
|
||||
<div class="queue-remaining in-progress">
|
||||
<div>
|
||||
Queued prompts: {$queueState.queueRemaining}.
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="queue-remaining done">
|
||||
<div>
|
||||
Nothing queued.
|
||||
</div>
|
||||
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
$pending-height: 300px;
|
||||
$pending-height: 200px;
|
||||
$bottom-bar-height: 70px;
|
||||
$mode-buttons-height: 30px;
|
||||
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height});
|
||||
|
||||
.queue {
|
||||
}
|
||||
|
||||
.queue-remaining {
|
||||
height: 5em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border: 5px solid #CCC;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
|
||||
.queue-entries {
|
||||
overflow-y: scroll;
|
||||
max-height: calc(100vh - $pending-height);
|
||||
height: $queue-height;
|
||||
max-height: $queue-height;
|
||||
}
|
||||
|
||||
.queue-entry {
|
||||
padding: 0.5rem;
|
||||
padding: 1.0rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 2px solid var(--neutral-200);
|
||||
border-bottom: 1px solid var(--neutral-400);
|
||||
border-bottom: 1px solid var(--panel-border-color);
|
||||
background: var(--panel-background-fill);
|
||||
|
||||
&.success {
|
||||
background: green
|
||||
/* background: green; */
|
||||
}
|
||||
&.error {
|
||||
background: red
|
||||
background: red;
|
||||
}
|
||||
&.cached {
|
||||
background: grey
|
||||
&.all_cached {
|
||||
background: grey;
|
||||
}
|
||||
&.running {
|
||||
/* background: lightblue; */
|
||||
}
|
||||
&.pending, &.unknown {
|
||||
/* background: orange; */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,54 +224,86 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queue-entry-message {
|
||||
width: var(--size-20);
|
||||
font-size: large;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.queue-entry-submessage {
|
||||
width: var(--size-20);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.queue-entry-queued-at {
|
||||
width: auto;
|
||||
font-size: 14px;
|
||||
font-size: 10px;
|
||||
position:absolute;
|
||||
right: 0px;
|
||||
bottom:0px;
|
||||
padding: 0.0rem 0.4rem;
|
||||
/* color: var(--neutral-600) */
|
||||
color: var(--neutral-600);
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
|
||||
.node-name {
|
||||
border: 5px solid #CCC;
|
||||
background-color: var(--color-red-300);
|
||||
padding: 0.2em;
|
||||
.mode-buttons {
|
||||
height: calc($mode-buttons-height);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.mode-button {
|
||||
padding: 0.2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--panel-border-color);
|
||||
font-weight: bold;
|
||||
background: var(--button-secondary-background-fill);
|
||||
|
||||
&:hover {
|
||||
background: var(--button-secondary-background-fill-hover);
|
||||
filter: brightness(120%);
|
||||
}
|
||||
&:active {
|
||||
filter: brightness(50%)
|
||||
}
|
||||
&.mode-selected {
|
||||
filter: brightness(150%)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
width: 100%;
|
||||
height: $pending-height;
|
||||
height: calc($pending-height);
|
||||
position: absolute;
|
||||
}
|
||||
border: 2px solid var(--panel-border-color);
|
||||
|
||||
.in-progress {
|
||||
background-color: var(--secondary-300);
|
||||
}
|
||||
.done {
|
||||
background-color: var(--color-grey-200);
|
||||
}
|
||||
.node-name {
|
||||
background-color: var(--comfy-node-name-background);
|
||||
color: var(--comfy-node-name-foreground);
|
||||
padding: 0.2em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
height: 1.5em;
|
||||
width: 10em;
|
||||
text-align: center;
|
||||
border: 1px solid black;
|
||||
.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 - $bottom-bar-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,16 +24,16 @@
|
||||
<style>
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
background-color: lightgrey;
|
||||
background: var(--comfy-progress-bar-background);
|
||||
padding: 0px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 100%;
|
||||
background-color: #B3D8A9;
|
||||
background: var(--comfy-progress-bar-foreground);
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
24
src/lib/components/Spinner.svelte
Normal file
24
src/lib/components/Spinner.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<span class="loader" {...$$restProps}/>
|
||||
|
||||
<style lang="scss">
|
||||
$border-bottom-color_1: var(--comfy-spinner-accent-color);
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 5px solid var(--comfy-spinner-main-color);
|
||||
border-bottom-color: $border-bottom-color_1;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 2s linear infinite;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user