Show prompt details & thumbnail in queue
This commit is contained in:
@@ -65,8 +65,6 @@ export class ImageViewer {
|
||||
setTimeout(() => {
|
||||
this.modalImage.focus()
|
||||
}, 200)
|
||||
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
static get_gallery_urls(galleryElem: HTMLDivElement): string[] {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
import EventEmitter from "events";
|
||||
import type { GalleryOutput } from "./nodes/ComfyWidgetNodes";
|
||||
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
|
||||
import type { SerializedLGraph } from "@litegraph-ts/core";
|
||||
|
||||
export type ComfyPromptRequest = {
|
||||
@@ -62,7 +62,8 @@ export type ComfyPromptPNGInfo = {
|
||||
export type ComfyPromptExtraData = {
|
||||
extra_pnginfo?: ComfyPromptPNGInfo,
|
||||
client_id?: string, // UUID
|
||||
subgraphs: string[]
|
||||
subgraphs: string[],
|
||||
thumbnails?: GalleryOutputEntry[]
|
||||
}
|
||||
|
||||
type ComfyAPIEvents = {
|
||||
|
||||
@@ -59,7 +59,8 @@ export type SerializedAppState = {
|
||||
export type SerializedPromptInput = [NodeID, number] | any
|
||||
|
||||
export type SerializedPromptInputs = {
|
||||
inputs: Record<NodeID, SerializedPromptInput>,
|
||||
/* property name -> value or link */
|
||||
inputs: Record<string, SerializedPromptInput>,
|
||||
class_type: string
|
||||
}
|
||||
|
||||
@@ -611,7 +612,7 @@ export default class ComfyApp {
|
||||
|
||||
// The reasoning behind this check:
|
||||
// We only want to serialize inputs to nodes with backend equivalents.
|
||||
// And in ComfyBox, the nodes in litegraph *never* have widgets, instead they're all inputs.
|
||||
// And in ComfyBox, the backend nodes in litegraph *never* have widgets, instead they're all inputs.
|
||||
// All values are passed by separate frontend-only nodes,
|
||||
// either UI-bound or something like ConstantInteger.
|
||||
// So we know that any value passed into a backend node *must* come from
|
||||
@@ -728,8 +729,26 @@ export default class ComfyApp {
|
||||
({ num, batchCount } = this.queueItems.pop());
|
||||
console.debug(`Queue get! ${num} ${batchCount} ${tag}`);
|
||||
|
||||
const thumbnails = []
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
if (node.mode !== NodeMode.ALWAYS
|
||||
|| (tag != null
|
||||
&& Array.isArray(node.properties.tags)
|
||||
&& node.properties.tags.indexOf(tag) === -1))
|
||||
continue;
|
||||
|
||||
if ("getPromptThumbnails" in node) {
|
||||
const thumbsToAdd = (node as ComfyGraphNode).getPromptThumbnails();
|
||||
if (thumbsToAdd)
|
||||
thumbnails.push(...thumbsToAdd)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
for (const node of this.lGraph._nodes_in_order) {
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
if (node.mode !== NodeMode.ALWAYS)
|
||||
continue;
|
||||
|
||||
if ("beforeQueued" in node) {
|
||||
(node as ComfyGraphNode).beforeQueued(tag);
|
||||
}
|
||||
@@ -742,7 +761,8 @@ export default class ComfyApp {
|
||||
extra_pnginfo: {
|
||||
workflow: p.workflow
|
||||
},
|
||||
subgraphs: [tag]
|
||||
subgraphs: [tag],
|
||||
thumbnails
|
||||
}
|
||||
|
||||
let error = null;
|
||||
@@ -871,7 +891,7 @@ export default class ComfyApp {
|
||||
const isComfyInput = isComfyComboInput(input)
|
||||
const isComfyCombo = isComfyComboNode(inputNode)
|
||||
|
||||
console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
|
||||
// console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
|
||||
|
||||
return isComfyCombo && isComfyInput
|
||||
});
|
||||
|
||||
@@ -480,10 +480,5 @@
|
||||
bottom: 0;
|
||||
padding: 0.5em; */
|
||||
}
|
||||
|
||||
:global(input[type=text]:disabled) {
|
||||
@include disable-input;
|
||||
}
|
||||
:global(textarea:disabled) {
|
||||
@include disable-input;
|
||||
|
||||
@include disable-inputs;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import queueState, { type CompletedQueueEntry, type QueueEntry, type QueueEntryStatus } from "$lib/stores/queueState";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
import Spinner from "./Spinner.svelte";
|
||||
import PromptDisplay from "./PromptDisplay.svelte";
|
||||
import { ListIcon as List } from "svelte-feather-icons";
|
||||
import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
@@ -10,6 +11,7 @@
|
||||
import { Button } from "@gradio/button";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
import { tick } from "svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
|
||||
let queuePending: Writable<QueueEntry[]> | null = null;
|
||||
let queueRunning: Writable<QueueEntry[]> | null = null;
|
||||
@@ -17,6 +19,7 @@
|
||||
let queueList: HTMLDivElement | null = null;
|
||||
|
||||
type QueueUIEntry = {
|
||||
entry: QueueEntry,
|
||||
message: string,
|
||||
submessage: string,
|
||||
date?: string,
|
||||
@@ -49,21 +52,17 @@
|
||||
updateFromHistory();
|
||||
}
|
||||
|
||||
function convertEntry(entry: QueueEntry): QueueUIEntry {
|
||||
const images = Object.values(entry.outputs).flatMap(o => o.images)
|
||||
.map(convertComfyOutputToComfyURL);
|
||||
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(", ")
|
||||
}
|
||||
|
||||
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);
|
||||
function convertEntry(entry: QueueEntry): QueueUIEntry {
|
||||
let date = entry.finishedAt || entry.queuedAt;
|
||||
let dateStr = null;
|
||||
if (date) {
|
||||
dateStr = formatDate(date);
|
||||
}
|
||||
|
||||
let message = "Prompt";
|
||||
@@ -72,24 +71,43 @@
|
||||
|
||||
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||
if (Object.keys(entry.outputs).length > 0) {
|
||||
submessage = `Images: ${Object.keys(entry.outputs).length}`
|
||||
const imageCount = Object.values(entry.outputs).flatMap(o => o.images).length
|
||||
submessage = `Images: ${imageCount}`
|
||||
}
|
||||
|
||||
return {
|
||||
entry,
|
||||
message,
|
||||
submessage,
|
||||
date,
|
||||
dateStr,
|
||||
status: "pending",
|
||||
images,
|
||||
images: []
|
||||
}
|
||||
}
|
||||
|
||||
function convertPendingEntry(entry: QueueEntry): QueueUIEntry {
|
||||
const result = convertEntry(entry);
|
||||
|
||||
const thumbnails = entry.extraData?.thumbnails
|
||||
if (thumbnails) {
|
||||
result.images = thumbnails.map(convertComfyOutputToComfyURL);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function convertCompletedEntry(entry: CompletedQueueEntry): QueueUIEntry {
|
||||
const result = convertEntry(entry.entry);
|
||||
result.status = entry.status;
|
||||
|
||||
const images = Object.values(entry.entry.outputs).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.details = entry.error
|
||||
|
||||
@@ -97,7 +115,7 @@
|
||||
}
|
||||
|
||||
async function updateFromQueue() {
|
||||
_entries = $queuePending.map(convertEntry).reverse(); // newest entries appear at the top
|
||||
_entries = $queuePending.map(convertPendingEntry).reverse(); // newest entries appear at the top
|
||||
if (queueList) {
|
||||
await tick(); // Wait for list size to be recalculated
|
||||
queueList.scroll({ top: queueList.scrollHeight })
|
||||
@@ -108,28 +126,45 @@
|
||||
async function updateFromHistory() {
|
||||
_entries = $queueCompleted.map(convertCompletedEntry).reverse();
|
||||
if (queueList) {
|
||||
await tick(); // Wait for list size to be recalculated
|
||||
queueList.scrollTo(0, 0);
|
||||
}
|
||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
|
||||
}
|
||||
|
||||
function showLightbox(entry: QueueUIEntry, e: Event) {
|
||||
function showLightbox(entry: QueueUIEntry, index: number, e: Event) {
|
||||
e.preventDefault()
|
||||
if (!entry.images)
|
||||
return
|
||||
|
||||
ImageViewer.instance.showModal(entry.images, 0)
|
||||
ImageViewer.instance.showModal(entry.images, index);
|
||||
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
async function interrupt() {
|
||||
if ($queueState.isInterrupting)
|
||||
return
|
||||
|
||||
const app = (window as any).app as ComfyApp;
|
||||
if (!app || !app.api)
|
||||
return;
|
||||
|
||||
await app.api.interrupt();
|
||||
await app.api.interrupt()
|
||||
.then(() => {
|
||||
queueState.update(s => { s.isInterrupting = true; return s })
|
||||
});
|
||||
}
|
||||
|
||||
let showModal = false;
|
||||
let selectedPrompt = null;
|
||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
||||
selectedPrompt = entry.entry.prompt;
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
$: if(!showModal)
|
||||
selectedPrompt = null;
|
||||
|
||||
let queued = false
|
||||
$: queued = Boolean($queueState.runningNodeID || $queueState.progress);
|
||||
|
||||
@@ -137,14 +172,32 @@
|
||||
$: 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>
|
||||
{#if selectedPrompt}
|
||||
<PromptDisplay prompt={selectedPrompt} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<div class="queue">
|
||||
<div class="queue-entries {mode}-mode" bind:this={queueList}>
|
||||
{#if _entries.length > 0}
|
||||
{#each _entries as entry}
|
||||
<div class="queue-entry {entry.status}">
|
||||
<div class="queue-entry {entry.status}" on:click={(e) => showPrompt(entry, e)}>
|
||||
{#if entry.images.length > 0}
|
||||
<div class="queue-entry-images" on:click={(e) => showLightbox(entry, e)}>
|
||||
<img class="queue-entry-image" src={entry.images[0]} alt="thumbnail" />
|
||||
<div class="queue-entry-images"
|
||||
style="--cols: {Math.ceil(Math.sqrt(Math.min(entry.images.length, 4)))}" >
|
||||
{#each entry.images.slice(0, 4) as image, i}
|
||||
<div>
|
||||
<img class="queue-entry-image"
|
||||
on:click={(e) => showLightbox(entry, i, e)}
|
||||
src={image}
|
||||
alt="thumbnail" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- <div class="queue-entry-image-placeholder" /> -->
|
||||
@@ -158,7 +211,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-entry-rest">
|
||||
<div class="queue-entry-rest {entry.status}">
|
||||
{#if entry.date != null}
|
||||
<span class="queue-entry-queued-at">
|
||||
{entry.date}
|
||||
@@ -212,7 +265,10 @@
|
||||
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
|
||||
</div>
|
||||
<div class="queue-action-buttons">
|
||||
<Button variant="secondary" on:click={interrupt} style={{ full_width: true }}>
|
||||
<Button variant="secondary"
|
||||
disabled={$queueState.isInterrupting}
|
||||
on:click={interrupt}
|
||||
style={{ full_width: true }}>
|
||||
Interrupt
|
||||
</Button>
|
||||
</div>
|
||||
@@ -226,6 +282,13 @@
|
||||
$mode-buttons-height: 30px;
|
||||
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height});
|
||||
|
||||
.prompt-modal-header {
|
||||
padding-left: 0.2rem;
|
||||
|
||||
h1 {
|
||||
font-size: large;
|
||||
}
|
||||
}
|
||||
.queue {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
@@ -275,16 +338,20 @@
|
||||
border-top: 1px solid var(--table-border-color);
|
||||
background: var(--panel-background-fill);
|
||||
|
||||
&:hover:not(:has(img:hover)) {
|
||||
background: var(--block-background-fill);
|
||||
}
|
||||
|
||||
&.success {
|
||||
/* background: green; */
|
||||
}
|
||||
&.error {
|
||||
background: red;
|
||||
}
|
||||
&.all_cached {
|
||||
&.all_cached, &.interrupted {
|
||||
filter: brightness(80%);
|
||||
background: var(--neutral-600);
|
||||
color: var(--neutral-300);
|
||||
background: var(--comfy-disabled-textbox-background-fill);
|
||||
color: var(--comfy-disable-textbox-text-color);
|
||||
}
|
||||
&.running {
|
||||
/* background: lightblue; */
|
||||
@@ -294,29 +361,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
.queue-entry-images {
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
margin: auto;
|
||||
.queue-entry-rest {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.all_cached, &.interrupted {
|
||||
filter: brightness(80%);
|
||||
color: var(--neutral-300);
|
||||
}
|
||||
}
|
||||
|
||||
$thumbnails-size: 12rem;
|
||||
|
||||
.queue-entry-images {
|
||||
--cols: 1;
|
||||
margin: auto;
|
||||
width: calc($thumbnails-size * 2);
|
||||
display: grid;
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(var(--cols), 1fr);
|
||||
grid-template-rows: repeat(var(--cols), 1fr);
|
||||
column-gap: 1px;
|
||||
row-gap: 1px;
|
||||
vertical-align: top;
|
||||
|
||||
img {
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
|
||||
> .queue-entry-image {
|
||||
filter: none;
|
||||
&:hover {
|
||||
filter: brightness(120%) contrast(120%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.queue-entry-image-placeholder {
|
||||
width: var(--size-20);
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.queue-entry-rest {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.queue-entry-details {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
@@ -378,6 +456,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(85%);
|
||||
}
|
||||
&:active {
|
||||
filter: brightness(50%)
|
||||
}
|
||||
&.mode-selected {
|
||||
filter: brightness(80%)
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark) .mode-button {
|
||||
filter: none;
|
||||
&:hover {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
61
src/lib/components/Modal.svelte
Normal file
61
src/lib/components/Modal.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script>
|
||||
export let showModal; // boolean
|
||||
|
||||
let dialog; // HTMLDialogElement
|
||||
|
||||
$: if (dialog && showModal) dialog.showModal();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<dialog
|
||||
bind:this={dialog}
|
||||
on:close={() => (showModal = false)}
|
||||
on:click|self={() => dialog.close()}
|
||||
>
|
||||
<div on:click|stopPropagation>
|
||||
<slot name="header" />
|
||||
<slot />
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<button autofocus on:click={() => dialog.close()}>close modal</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
dialog {
|
||||
max-width: 75vw;
|
||||
border-radius: 0.2em;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
dialog > div {
|
||||
padding: 1em;
|
||||
}
|
||||
dialog[open] {
|
||||
animation: zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@keyframes zoom {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
dialog[open]::backdrop {
|
||||
animation: fade 0.2s ease-out;
|
||||
}
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
165
src/lib/components/PromptDisplay.svelte
Normal file
165
src/lib/components/PromptDisplay.svelte
Normal file
@@ -0,0 +1,165 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import { TextBox } from "@gradio/form";
|
||||
import type { SerializedPromptInput, SerializedPromptInputsAll } from "./ComfyApp";
|
||||
import { Block, BlockLabel, BlockTitle } from "@gradio/atoms";
|
||||
import { JSON as JSONComponent } from "@gradio/json";
|
||||
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
|
||||
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
||||
|
||||
const splitLength = 50;
|
||||
|
||||
export let prompt: SerializedPromptInputsAll;
|
||||
|
||||
function isInputLink(input: SerializedPromptInput): boolean {
|
||||
return Array.isArray(input)
|
||||
&& input.length === 2
|
||||
&& typeof input[0] === "string"
|
||||
&& typeof input[1] === "number"
|
||||
}
|
||||
|
||||
function countNewLines(str: string): number {
|
||||
return str.split(/\r\n|\r|\n/).length
|
||||
}
|
||||
|
||||
function isMultiline(input: any): boolean {
|
||||
return typeof input === "string" && (input.length > splitLength || countNewLines(input) > 1);
|
||||
}
|
||||
|
||||
function formatInput(input: any): string {
|
||||
if (typeof input === "string")
|
||||
return input
|
||||
return JSON.stringify(input, null, 2);
|
||||
}
|
||||
|
||||
let copiedNodeID: any | null = null;
|
||||
let copiedInputName: string | null = null;
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
function copyFeedback(nodeID: string, inputName: string) {
|
||||
copiedNodeID = nodeID;
|
||||
copiedInputName = inputName;
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
copiedNodeID = null;
|
||||
copiedInputName = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function handleCopy(nodeID: string, inputName: string, input: any) {
|
||||
if ("clipboard" in navigator) {
|
||||
await navigator.clipboard.writeText(formatInput(input));
|
||||
copyFeedback(nodeID, inputName);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="prompt-display">
|
||||
<Block>
|
||||
{#each Object.entries(prompt) as [nodeID, inputs], i}
|
||||
{@const classType = inputs.class_type}
|
||||
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
|
||||
{#if filtered.length > 0}
|
||||
<div class="accordion">
|
||||
<Block padding={true}>
|
||||
<Accordion label="Node {nodeID}: {classType}" open={false}>
|
||||
{#each filtered as [inputName, input]}
|
||||
<Block>
|
||||
<BlockTitle>Input: {inputName}</BlockTitle>
|
||||
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
|
||||
{#if copiedNodeID === nodeID && copiedInputName === inputName}
|
||||
<span class="copied-icon">
|
||||
<Check />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="copy-text"><Copy /></span>
|
||||
{/if}
|
||||
</button>
|
||||
<div>
|
||||
{#if isInputLink(input)}
|
||||
Link {input[0]} -> {input[1]}
|
||||
{:else if typeof input === "object"}
|
||||
<Block>
|
||||
<BlockLabel
|
||||
Icon={JSONIcon}
|
||||
show_label={true}
|
||||
label={inputName}
|
||||
float={true}
|
||||
/>
|
||||
<JSONComponent value={input} />
|
||||
</Block>
|
||||
{:else if isMultiline(input)}
|
||||
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
|
||||
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
|
||||
{:else}
|
||||
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
|
||||
{/if}
|
||||
</div>
|
||||
</Block>
|
||||
{/each}
|
||||
</Accordion>
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Block>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.prompt-display {
|
||||
overflow-y: scroll;
|
||||
width: 50vw;
|
||||
height: 60vh;
|
||||
color: none;
|
||||
|
||||
.copy-button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: var(--block-label-margin);
|
||||
right: var(--block-label-margin);
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-drop);
|
||||
border: 1px solid var(--border-color-primary);
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-radius: var(--block-label-right-radius);
|
||||
background: var(--block-label-background-fill);
|
||||
padding: 5px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
color: var(--block-label-text-color);
|
||||
font: var(--font);
|
||||
font-size: var(--button-small-text-size);
|
||||
}
|
||||
|
||||
@keyframes -light-up {
|
||||
from {
|
||||
color: var(--color-yellow-400);
|
||||
}
|
||||
to {
|
||||
color: none;
|
||||
}
|
||||
}
|
||||
|
||||
.copied-icon {
|
||||
animation: light-up 1s ease-out;
|
||||
:global(.svelte-select) {
|
||||
animation: light-up 1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
|
||||
.accordion {
|
||||
background: var(--panel-background-fill);
|
||||
|
||||
|
||||
:global(> .block .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:click={handleClick} class="label-wrap" class:open>
|
||||
<div class="label-wrap" on:click={handleClick} class:open>
|
||||
<span>{label}</span>
|
||||
<span style:transform={open ? "rotate(0)" : "rotate(90deg)"} class="icon">
|
||||
▼
|
||||
|
||||
@@ -7,6 +7,7 @@ import { get } from "svelte/store";
|
||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
|
||||
import type { NotifyOptions } from "$lib/notify";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import { convertComfyOutputToGradio, uploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||
|
||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||
@@ -659,3 +660,54 @@ LiteGraph.registerNodeType({
|
||||
desc: "Uploads an image from the specified ComfyUI folder into its input folder",
|
||||
type: "actions/store_images"
|
||||
})
|
||||
|
||||
export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties {
|
||||
defaultFolderType: string | null
|
||||
}
|
||||
|
||||
export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
|
||||
override properties: ComfySetPromptThumbnailsActionProperties = {
|
||||
tags: [],
|
||||
defaultFolderType: "input",
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "filenames", type: "*" },
|
||||
]
|
||||
}
|
||||
|
||||
_value: any = null;
|
||||
|
||||
override getPromptThumbnails(): GalleryOutputEntry[] | null {
|
||||
const data = this.getInputData(0)
|
||||
|
||||
const folderType = this.properties.folderType || "input";
|
||||
|
||||
const convertString = (s: string): GalleryOutputEntry => {
|
||||
return { filename: data, subfolder: "", type: folderType }
|
||||
}
|
||||
|
||||
if (typeof data === "string") {
|
||||
return [convertString(data)]
|
||||
}
|
||||
else if (data != null && typeof data === "object") {
|
||||
if ("filename" in data && "type" in data)
|
||||
return [data as GalleryOutputEntry];
|
||||
}
|
||||
else if (Array.isArray(data) && data.length > 0) {
|
||||
if (typeof data[0] === "string")
|
||||
return data.map(convertString)
|
||||
else if (typeof data[0] === "object" && "filename" in data[0] && "type" in data[0])
|
||||
return data as GalleryOutputEntry[]
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfySetPromptThumbnailsAction,
|
||||
title: "Comfy.SetPromptThumbnailsAction",
|
||||
desc: "When a subgraph containing this node is executed, sets the thumbnails in the queue sidebar to the input filename(s).",
|
||||
type: "actions/set_prompt_thumbnails"
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
||||
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes";
|
||||
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { get } from "svelte/store";
|
||||
@@ -50,6 +50,13 @@ export default class ComfyGraphNode extends LGraphNode {
|
||||
*/
|
||||
onExecuted?(output: GalleryOutput): void;
|
||||
|
||||
/*
|
||||
* When a prompt is queued, this will be called on the node if it can
|
||||
* provide any thumbnails for use with the prompt queue. Useful for HR Fix
|
||||
* or img2img workloads.
|
||||
*/
|
||||
getPromptThumbnails?(): GalleryOutputEntry[] | null
|
||||
|
||||
/*
|
||||
* Allows you to manually specify an auto-config for certain input slot
|
||||
* indices, so that when a ComfyWidgetNode is connected to the input slot it
|
||||
|
||||
@@ -54,7 +54,8 @@ export type QueueState = {
|
||||
queueCompleted: Writable<CompletedQueueEntry[]>,
|
||||
queueRemaining: number | "X" | null;
|
||||
runningNodeID: number | null;
|
||||
progress: Progress | null
|
||||
progress: Progress | null,
|
||||
isInterrupting: boolean
|
||||
}
|
||||
type WritableQueueStateStore = Writable<QueueState> & QueueStateOps;
|
||||
|
||||
@@ -64,7 +65,8 @@ const store: Writable<QueueState> = writable({
|
||||
queueCompleted: writable([]),
|
||||
queueRemaining: null,
|
||||
runningNodeID: null,
|
||||
progress: null
|
||||
progress: null,
|
||||
isInterrupting: false
|
||||
})
|
||||
|
||||
function toQueueEntry(resp: ComfyAPIHistoryItem): QueueEntry {
|
||||
@@ -209,6 +211,7 @@ function executionCached(promptID: PromptID, nodes: NodeID[]) {
|
||||
else {
|
||||
console.error("[queueState] Could not find in pending! (executionCached)", promptID, "pending", JSON.stringify(get(get(store).queuePending).map(p => p.promptID)), "running", JSON.stringify(get(get(store).queueRunning).map(p => p.promptID)))
|
||||
}
|
||||
s.isInterrupting = false; // TODO move to start
|
||||
s.progress = null;
|
||||
s.runningNodeID = null;
|
||||
return s
|
||||
@@ -248,6 +251,7 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp
|
||||
}
|
||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||
console.debug("[queueState] ADD PROMPT", promptID)
|
||||
s.isInterrupting = false;
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,12 +51,7 @@
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
|
||||
:global(input[type=text]:disabled) {
|
||||
@include disable-input;
|
||||
}
|
||||
:global(textarea:disabled) {
|
||||
@include disable-input;
|
||||
}
|
||||
@include disable-inputs;
|
||||
}
|
||||
|
||||
:global(span.hide) {
|
||||
|
||||
@@ -14,7 +14,11 @@ body {
|
||||
:root {
|
||||
--color-blue-500: #3985f5;
|
||||
|
||||
--comfy-accent-soft: var(--neutral-400);
|
||||
--comfy-accent-soft: var(--neutral-300);
|
||||
--comfy-disabled-label-color: var(--neutral-400);
|
||||
--comfy-disabled-textbox-background-fill: var(--neutral-200);
|
||||
--comfy-disabled-textbox-border-color: var(--neutral-300);
|
||||
--comfy-disabled-textbox-text-color: var(--neutral-500);
|
||||
--comfy-splitpanes-background-fill: var(--secondary-100);
|
||||
--comfy-splitpanes-background-fill-hover: var(--secondary-300);
|
||||
--comfy-splitpanes-background-fill-active: var(--secondary-400);
|
||||
@@ -34,6 +38,10 @@ body {
|
||||
color-scheme: dark;
|
||||
|
||||
--comfy-accent-soft: var(--neutral-600);
|
||||
--comfy-disabled-label-color: var(--neutral-500);
|
||||
--comfy-disabled-textbox-background-fill: var(--neutral-800);
|
||||
--comfy-disabled-textbox-border-color: var(--neutral-700);
|
||||
--comfy-disabled-textbox-text-color: var(--neutral-500);
|
||||
--comfy-splitpanes-background-fill: var(--panel-border-color);
|
||||
--comfy-splitpanes-background-fill-hover: var(--secondary-500);
|
||||
--comfy-splitpanes-background-fill-active: var(--secondary-300);
|
||||
@@ -56,13 +64,32 @@ body {
|
||||
}
|
||||
|
||||
@mixin disable-input {
|
||||
-webkit-text-fill-color: var(--neutral-500);
|
||||
background-color: var(--neutral-200);
|
||||
border-color: var(--neutral-300);
|
||||
-webkit-text-fill-color: var(--comfy-disabled-textbox-text-color);
|
||||
background-color: var(--comfy-disabled-textbox-background-fill);
|
||||
border-color: var(--comfy-disabled-textbox-border-color);
|
||||
box-shadow: 0 0 0 var(--shadow-spread) transparent, rgba(0, 0, 0, 0.08) 0px 2px 4px 0px inset;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@mixin disable-inputs {
|
||||
:global(input[type=text]:disabled) {
|
||||
@include disable-input;
|
||||
}
|
||||
:global(textarea:disabled) {
|
||||
@include disable-input;
|
||||
}
|
||||
:global(label:has(input:disabled) > span) {
|
||||
color: var(--comfy-disabled-label-color);
|
||||
}
|
||||
:global(label:has(textarea:disabled) > span) {
|
||||
color: var(--comfy-disabled-label-color);
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--panel-border-color);
|
||||
}
|
||||
|
||||
select {
|
||||
color: var(--body-text-color);
|
||||
background: var(--block-background-fill);
|
||||
|
||||
Reference in New Issue
Block a user