Queue/history bar
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -9,7 +8,7 @@
|
|||||||
<meta name="theme-color" content="#2196f3">
|
<meta name="theme-color" content="#2196f3">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app" class="mobile">
|
||||||
<script type="module" src='/src/main-mobile.ts'></script>
|
<script type="module" src='/src/main-mobile.ts'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { Progress, SerializedPrompt, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
|
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type { GalleryOutput } from "./nodes/ComfyWidgetNodes";
|
import type { GalleryOutput } from "./nodes/ComfyWidgetNodes";
|
||||||
|
import type { SerializedLGraph } from "@litegraph-ts/core";
|
||||||
|
|
||||||
type PromptRequestBody = {
|
export type ComfyPromptRequest = {
|
||||||
client_id: string,
|
client_id?: string,
|
||||||
prompt: any,
|
prompt: SerializedPromptInputsAll,
|
||||||
extra_data: any,
|
extra_data: ComfyPromptExtraData,
|
||||||
front: boolean,
|
front?: boolean,
|
||||||
number: number | undefined
|
number?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueueItemType = "queue" | "history";
|
export type QueueItemType = "queue" | "history";
|
||||||
@@ -34,8 +35,8 @@ export type PromptID = string; // UUID
|
|||||||
export type ComfyAPIHistoryItem = [
|
export type ComfyAPIHistoryItem = [
|
||||||
number, // prompt number
|
number, // prompt number
|
||||||
PromptID,
|
PromptID,
|
||||||
SerializedPrompt,
|
SerializedPromptInputsAll,
|
||||||
any, // extra data
|
ComfyPromptExtraData,
|
||||||
NodeID[] // good outputs
|
NodeID[] // good outputs
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -54,6 +55,16 @@ export type ComfyAPIHistoryResponse = {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ComfyPromptPNGInfo = {
|
||||||
|
workflow: SerializedLGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComfyPromptExtraData = {
|
||||||
|
extra_pnginfo?: ComfyPromptPNGInfo,
|
||||||
|
client_id?: string, // UUID
|
||||||
|
subgraphs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
type ComfyAPIEvents = {
|
type ComfyAPIEvents = {
|
||||||
status: (status: ComfyAPIStatusResponse | null, error?: Error | null) => void,
|
status: (status: ComfyAPIStatusResponse | null, error?: Error | null) => void,
|
||||||
progress: (progress: Progress) => void,
|
progress: (progress: Progress) => void,
|
||||||
@@ -219,19 +230,11 @@ export default class ComfyAPI {
|
|||||||
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
||||||
* @param {object} prompt The prompt data to queue
|
* @param {object} prompt The prompt data to queue
|
||||||
*/
|
*/
|
||||||
async queuePrompt(number: number, { output, workflow }, extra_data: any): Promise<ComfyAPIPromptResponse> {
|
async queuePrompt(body: ComfyPromptRequest): Promise<ComfyAPIPromptResponse> {
|
||||||
const body: PromptRequestBody = {
|
body.client_id = this.clientId;
|
||||||
client_id: this.clientId,
|
|
||||||
prompt: output,
|
|
||||||
extra_data,
|
|
||||||
front: false,
|
|
||||||
number: number
|
|
||||||
};
|
|
||||||
|
|
||||||
if (number === -1) {
|
if (body.number === -1) {
|
||||||
body.front = true;
|
body.front = true;
|
||||||
} else if (number != 0) {
|
|
||||||
body.number = number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let postBody = null;
|
let postBody = null;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let propsSidebarSize = 15; //15;
|
let propsSidebarSize = 0;
|
||||||
|
|
||||||
function toggleProps() {
|
function toggleProps() {
|
||||||
if (propsSidebarSize == 0) {
|
if (propsSidebarSize == 0) {
|
||||||
@@ -90,11 +90,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let queueSidebarSize = 15;
|
let queueSidebarSize = 20;
|
||||||
|
|
||||||
function toggleQueue() {
|
function toggleQueue() {
|
||||||
if (queueSidebarSize == 0) {
|
if (queueSidebarSize == 0) {
|
||||||
queueSidebarSize = 15;
|
queueSidebarSize = 20;
|
||||||
app.resizeCanvas();
|
app.resizeCanvas();
|
||||||
}
|
}
|
||||||
else {
|
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 { 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 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 { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
@@ -385,6 +385,7 @@ export default class ComfyApp {
|
|||||||
const queue = await this.api.getQueue();
|
const queue = await this.api.getQueue();
|
||||||
const history = await this.api.getHistory();
|
const history = await this.api.getHistory();
|
||||||
queueState.queueUpdated(queue);
|
queueState.queueUpdated(queue);
|
||||||
|
queueState.historyUpdated(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
private requestPermissions() {
|
private requestPermissions() {
|
||||||
@@ -739,13 +740,24 @@ export default class ComfyApp {
|
|||||||
const p = await this.graphToPrompt(tag);
|
const p = await this.graphToPrompt(tag);
|
||||||
console.debug(promptToGraphVis(p))
|
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 error = null;
|
||||||
let promptID = null;
|
let promptID = null;
|
||||||
|
|
||||||
|
const request: ComfyPromptRequest = {
|
||||||
|
number: num,
|
||||||
|
extra_data: extraData,
|
||||||
|
prompt: p.output
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.api.queuePrompt(num, p, extra_data);
|
const response = await this.api.queuePrompt(request);
|
||||||
promptID = response.promptID;
|
promptID = response.promptID;
|
||||||
error = response.error;
|
error = response.error;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -768,7 +780,7 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lCanvas.draw(true, true);
|
this.lCanvas.draw(true, true);
|
||||||
queueState.afterQueued(promptID, num, p, extra_data)
|
queueState.afterQueued(promptID, num, p.output, extraData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,74 +1,113 @@
|
|||||||
<script lang="ts">
|
<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 ProgressBar from "./ProgressBar.svelte";
|
||||||
import { getNodeInfo } from "$lib/utils"
|
import Spinner from "./Spinner.svelte";
|
||||||
import type { Writable } from "svelte/store";
|
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 queuePending: Writable<QueueEntry[]> | null = null;
|
||||||
let queueRunning: Writable<QueueEntry[]> | null = null;
|
let queueRunning: Writable<QueueEntry[]> | null = null;
|
||||||
|
let queueCompleted: Writable<CompletedQueueEntry[]> | null = null;
|
||||||
|
|
||||||
type QueueUIEntry = {
|
type QueueUIEntry = {
|
||||||
message: string,
|
message: string,
|
||||||
submessage: string,
|
submessage: string,
|
||||||
date?: string,
|
date?: string,
|
||||||
status: "success" | "error" | "pending" | "running" | "all_cached",
|
status: QueueEntryStatus | "pending" | "running",
|
||||||
images?: string[] // URLs
|
images?: string[], // URLs
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($queueState) {
|
$: if ($queueState) {
|
||||||
queuePending = $queueState.queuePending
|
queuePending = $queueState.queuePending
|
||||||
queueRunning = $queueState.queueRunning
|
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[] = []
|
let _entries: QueueUIEntry[] = []
|
||||||
|
|
||||||
$: if ($queuePending && $queuePending.length != _entries.length) {
|
$: if (mode === "queue" && $queuePending && $queuePending.length != _entries.length) {
|
||||||
_entries = []
|
updateFromQueue();
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="queue">
|
<div class="queue">
|
||||||
<div class="queue-entries">
|
<div class="queue-entries">
|
||||||
{#each _entries as entry}
|
{#each _entries as entry}
|
||||||
<div class="queue-entry {entry.status}">
|
<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" />
|
<img class="queue-entry-image" src={entry.images[0]} alt="thumbnail" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="queue-entry-image-placeholder" />
|
<!-- <div class="queue-entry-image-placeholder" /> -->
|
||||||
{/if}
|
{/if}
|
||||||
<div class="queue-entry-details">
|
<div class="queue-entry-details">
|
||||||
<div class="queue-entry-message">
|
<div class="queue-entry-message">
|
||||||
@@ -88,68 +127,79 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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">
|
<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">
|
<div class="node-name">
|
||||||
<span>Node: {getNodeInfo($queueState.runningNodeID)}</span>
|
<span>Node: {getNodeInfo($queueState.runningNodeID)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} styles="height: 30px;" />
|
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<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 {
|
||||||
}
|
color: var(--body-text-color);
|
||||||
|
|
||||||
.queue-remaining {
|
|
||||||
height: 5em;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
border: 5px solid #CCC;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entries {
|
.queue-entries {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
max-height: calc(100vh - $pending-height);
|
height: $queue-height;
|
||||||
|
max-height: $queue-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entry {
|
.queue-entry {
|
||||||
padding: 0.5rem;
|
padding: 1.0rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-top: 2px solid var(--neutral-200);
|
border-bottom: 1px solid var(--panel-border-color);
|
||||||
border-bottom: 1px solid var(--neutral-400);
|
background: var(--panel-background-fill);
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background: green
|
/* background: green; */
|
||||||
}
|
}
|
||||||
&.error {
|
&.error {
|
||||||
background: red
|
background: red;
|
||||||
}
|
}
|
||||||
&.cached {
|
&.all_cached {
|
||||||
background: grey
|
background: grey;
|
||||||
|
}
|
||||||
|
&.running {
|
||||||
|
/* background: lightblue; */
|
||||||
|
}
|
||||||
|
&.pending, &.unknown {
|
||||||
|
/* background: orange; */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,54 +224,86 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entry-message {
|
.queue-entry-message {
|
||||||
width: var(--size-20);
|
font-size: 15px;
|
||||||
font-size: large;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entry-submessage {
|
.queue-entry-submessage {
|
||||||
width: var(--size-20);
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-entry-queued-at {
|
.queue-entry-queued-at {
|
||||||
width: auto;
|
width: auto;
|
||||||
font-size: 14px;
|
font-size: 10px;
|
||||||
position:absolute;
|
position:absolute;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
bottom:0px;
|
bottom:0px;
|
||||||
padding: 0.0rem 0.4rem;
|
padding: 0.0rem 0.4rem;
|
||||||
/* color: var(--neutral-600) */
|
color: var(--body-text-color);
|
||||||
color: var(--neutral-600);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-name {
|
.mode-buttons {
|
||||||
border: 5px solid #CCC;
|
height: calc($mode-buttons-height);
|
||||||
background-color: var(--color-red-300);
|
|
||||||
padding: 0.2em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
text-align: center;
|
||||||
justify-content: 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 {
|
.bottom {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $pending-height;
|
height: calc($pending-height);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
border: 2px solid var(--panel-border-color);
|
||||||
|
|
||||||
.in-progress {
|
.node-name {
|
||||||
background-color: var(--secondary-300);
|
background-color: var(--comfy-node-name-background);
|
||||||
}
|
color: var(--comfy-node-name-foreground);
|
||||||
.done {
|
padding: 0.2em;
|
||||||
background-color: var(--color-grey-200);
|
display: flex;
|
||||||
}
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.queue-item {
|
.queue-remaining {
|
||||||
height: 1.5em;
|
height: calc($pending-height - $bottom-bar-height - 50px);
|
||||||
width: 10em;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid black;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -24,16 +24,16 @@
|
|||||||
<style>
|
<style>
|
||||||
.progress {
|
.progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: lightgrey;
|
background: var(--comfy-progress-bar-background);
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #B3D8A9;
|
background: var(--comfy-progress-bar-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ComfyAPIHistoryItem, ComfyAPIQueueResponse, ComfyAPIStatusResponse, NodeID, PromptID } from "$lib/api";
|
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyPromptExtraData, NodeID, PromptID } from "$lib/api";
|
||||||
import type { Progress, SerializedPrompt, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
import type { Progress, SerializedPrompt, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
||||||
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
|
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
@@ -7,33 +7,39 @@ export type QueueItem = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueueEntryStatus = "success" | "error" | "all_cached" | "unknown";
|
||||||
|
|
||||||
type QueueStateOps = {
|
type QueueStateOps = {
|
||||||
queueUpdated: (queue: ComfyAPIQueueResponse) => void,
|
queueUpdated: (resp: ComfyAPIQueueResponse) => void,
|
||||||
|
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
||||||
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
||||||
executingUpdated: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
|
executingUpdated: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
|
||||||
executionCached: (promptID: PromptID, nodes: NodeID[]) => void,
|
executionCached: (promptID: PromptID, nodes: NodeID[]) => void,
|
||||||
executionError: (promptID: PromptID, message: string) => void,
|
executionError: (promptID: PromptID, message: string) => void,
|
||||||
progressUpdated: (progress: Progress) => void
|
progressUpdated: (progress: Progress) => void
|
||||||
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPrompt, extraData: any) => void
|
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
||||||
onExecuted: (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => void
|
onExecuted: (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueueEntry = {
|
export type QueueEntry = {
|
||||||
|
/* Data preserved on page refresh */
|
||||||
number: number,
|
number: number,
|
||||||
queuedAt?: Date,
|
queuedAt?: Date,
|
||||||
finishedAt?: Date,
|
finishedAt?: Date,
|
||||||
promptID: PromptID,
|
promptID: PromptID,
|
||||||
prompt: SerializedPrompt,
|
prompt: SerializedPromptInputsAll,
|
||||||
extraData: any,
|
extraData: ComfyPromptExtraData,
|
||||||
goodOutputs: NodeID[],
|
goodOutputs: NodeID[],
|
||||||
|
|
||||||
// Collected while the prompt is still executing
|
/* Data not sent by Comfy's API, lost on page refresh */
|
||||||
|
/* Prompt outputs, collected while the prompt is still executing */
|
||||||
outputs: SerializedPromptOutputs,
|
outputs: SerializedPromptOutputs,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompletedQueueEntry = {
|
export type CompletedQueueEntry = {
|
||||||
entry: QueueEntry,
|
entry: QueueEntry,
|
||||||
type: "success" | "error" | "all_cached",
|
status: QueueEntryStatus,
|
||||||
error?: string,
|
error?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +76,31 @@ function toQueueEntry(resp: ComfyAPIHistoryItem): QueueEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueUpdated(queue: ComfyAPIQueueResponse) {
|
function toCompletedQueueEntry(resp: ComfyAPIHistoryEntry): CompletedQueueEntry {
|
||||||
console.debug("[queueState] queueUpdated", queue.running.length, queue.pending.length)
|
const entry = toQueueEntry(resp.prompt)
|
||||||
|
entry.outputs = resp.outputs;
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
status: Object.values(entry.outputs).length > 0 ? "success" : "all_cached",
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueUpdated(resp: ComfyAPIQueueResponse) {
|
||||||
|
console.debug("[queueState] queueUpdated", resp.running.length, resp.pending.length)
|
||||||
store.update((s) => {
|
store.update((s) => {
|
||||||
s.queueRunning.set(queue.running.map(toQueueEntry));
|
s.queueRunning.set(resp.running.map(toQueueEntry));
|
||||||
s.queuePending.set(queue.pending.map(toQueueEntry));
|
s.queuePending.set(resp.pending.map(toQueueEntry));
|
||||||
s.queueRemaining = queue.pending.length;
|
s.queueRemaining = resp.pending.length;
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function historyUpdated(resp: ComfyAPIHistoryResponse) {
|
||||||
|
console.debug("[queueState] historyUpdated", Object.values(resp.history).length)
|
||||||
|
store.update((s) => {
|
||||||
|
const values = Object.values(resp.history) // TODO Order by prompt finished date!
|
||||||
|
s.queueCompleted.set(values.map(toCompletedQueueEntry));
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -115,7 +140,7 @@ function executingUpdated(promptID: PromptID | null, runningNodeID: NodeID | nul
|
|||||||
s.queueCompleted.update(qc => {
|
s.queueCompleted.update(qc => {
|
||||||
const completed: CompletedQueueEntry = {
|
const completed: CompletedQueueEntry = {
|
||||||
entry,
|
entry,
|
||||||
type: "success",
|
status: "success",
|
||||||
}
|
}
|
||||||
qc.push(completed)
|
qc.push(completed)
|
||||||
return qc
|
return qc
|
||||||
@@ -142,7 +167,7 @@ function executionCached(promptID: PromptID, nodes: NodeID[]) {
|
|||||||
s.queueCompleted.update(qc => {
|
s.queueCompleted.update(qc => {
|
||||||
const completed: CompletedQueueEntry = {
|
const completed: CompletedQueueEntry = {
|
||||||
entry,
|
entry,
|
||||||
type: "all_cached",
|
status: "all_cached",
|
||||||
}
|
}
|
||||||
qc.push(completed)
|
qc.push(completed)
|
||||||
return qc
|
return qc
|
||||||
@@ -167,7 +192,7 @@ function executionError(promptID: PromptID, message: string) {
|
|||||||
s.queueCompleted.update(qc => {
|
s.queueCompleted.update(qc => {
|
||||||
const completed: CompletedQueueEntry = {
|
const completed: CompletedQueueEntry = {
|
||||||
entry,
|
entry,
|
||||||
type: "error",
|
status: "error",
|
||||||
error: message
|
error: message
|
||||||
}
|
}
|
||||||
qc.push(completed)
|
qc.push(completed)
|
||||||
@@ -180,8 +205,8 @@ function executionError(promptID: PromptID, message: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPrompt, extraData: any) {
|
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
|
||||||
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt.workflow.nodes))
|
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt.nodes))
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const entry: QueueEntry = {
|
const entry: QueueEntry = {
|
||||||
number,
|
number,
|
||||||
@@ -215,6 +240,7 @@ const queueStateStore: WritableQueueStateStore =
|
|||||||
{
|
{
|
||||||
...store,
|
...store,
|
||||||
queueUpdated,
|
queueUpdated,
|
||||||
|
historyUpdated,
|
||||||
statusUpdated,
|
statusUpdated,
|
||||||
progressUpdated,
|
progressUpdated,
|
||||||
executingUpdated,
|
executingUpdated,
|
||||||
|
|||||||
@@ -135,6 +135,12 @@ export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileDat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertComfyOutputToComfyURL(output: GalleryOutputEntry): string {
|
||||||
|
const params = new URLSearchParams(output)
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
return url + "/view?" + params
|
||||||
|
}
|
||||||
|
|
||||||
export function convertFilenameToComfyURL(filename: string,
|
export function convertFilenameToComfyURL(filename: string,
|
||||||
subfolder: string = "",
|
subfolder: string = "",
|
||||||
type: "input" | "output" | "temp" = "output"): string {
|
type: "input" | "output" | "temp" = "output"): string {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<!-- <link rel="icon" href="%sveltekit.assets%/favicon.png" /> -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
<meta name="theme-color" content="#2196f3">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<script type="module" src='/src/main-mobile.ts'></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -21,6 +21,12 @@ body {
|
|||||||
--comfy-dropdown-item-background-hover: var(--neutral-400);
|
--comfy-dropdown-item-background-hover: var(--neutral-400);
|
||||||
--comfy-dropdown-item-color-active: var(--neutral-100);
|
--comfy-dropdown-item-color-active: var(--neutral-100);
|
||||||
--comfy-dropdown-item-background-active: var(--secondary-600);
|
--comfy-dropdown-item-background-active: var(--secondary-600);
|
||||||
|
--comfy-progress-bar-background: var(--secondary-300);
|
||||||
|
--comfy-progress-bar-foreground: #var(--body-text-color);
|
||||||
|
--comfy-node-name-background: var(--color-red-300);
|
||||||
|
--comfy-node-name-foreground: var(--body-text-color);
|
||||||
|
--comfy-spinner-main-color: var(--neutral-400);
|
||||||
|
--comfy-spinner-accent-color: var(--secondary-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -34,6 +40,17 @@ body {
|
|||||||
--comfy-dropdown-item-background-hover: var(--neutral-600);
|
--comfy-dropdown-item-background-hover: var(--neutral-600);
|
||||||
--comfy-dropdown-item-background-active: var(--secondary-600);
|
--comfy-dropdown-item-background-active: var(--secondary-600);
|
||||||
--comfy-dropdown-border-color: var(--neutral-600);
|
--comfy-dropdown-border-color: var(--neutral-600);
|
||||||
|
--comfy-progress-bar-background: var(--secondary-400);
|
||||||
|
--comfy-progress-bar-foreground: #var(--body-text-color);
|
||||||
|
--comfy-node-name-background: var(--primary-500);
|
||||||
|
--comfy-node-name-foreground: var(--body-text-color);
|
||||||
|
--comfy-spinner-main-color: var(--neutral-600);
|
||||||
|
--comfy-spinner-accent-color: var(--secondary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
--comfy-progress-bar-background: lightgrey;
|
||||||
|
--comfy-progress-bar-foreground: #B3D8A9
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin disable-input {
|
@mixin disable-input {
|
||||||
|
|||||||
Reference in New Issue
Block a user