Use UUIDs everywhere & improve prompt popup
This commit is contained in:
@@ -2,12 +2,13 @@ import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedProm
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
import EventEmitter from "events";
|
||||
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
|
||||
import type { SerializedLGraph } from "@litegraph-ts/core";
|
||||
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
||||
import type { SerializedLayoutState } from "./stores/layoutState";
|
||||
|
||||
export type ComfyPromptRequest = {
|
||||
client_id?: string,
|
||||
prompt: SerializedPromptInputsAll,
|
||||
extra_data: ComfyPromptExtraData,
|
||||
extra_data: ComfyBoxPromptExtraData,
|
||||
front?: boolean,
|
||||
number?: number
|
||||
}
|
||||
@@ -29,14 +30,14 @@ export type ComfyAPIQueueResponse = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type NodeID = string;
|
||||
export type PromptID = string; // UUID
|
||||
export type NodeID = UUID;
|
||||
export type PromptID = UUID; // UUID
|
||||
|
||||
export type ComfyAPIHistoryItem = [
|
||||
number, // prompt number
|
||||
PromptID,
|
||||
SerializedPromptInputsAll,
|
||||
ComfyPromptExtraData,
|
||||
ComfyBoxPromptExtraData,
|
||||
NodeID[] // good outputs
|
||||
]
|
||||
|
||||
@@ -56,14 +57,18 @@ export type ComfyAPIHistoryResponse = {
|
||||
}
|
||||
|
||||
export type ComfyPromptPNGInfo = {
|
||||
workflow: SerializedLGraph
|
||||
workflow: SerializedLGraph,
|
||||
comfyBoxLayout: SerializedLayoutState,
|
||||
comfyBoxSubgraphs: string[],
|
||||
}
|
||||
|
||||
export type ComfyPromptExtraData = {
|
||||
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
|
||||
thumbnails?: GalleryOutputEntry[],
|
||||
}
|
||||
|
||||
export type ComfyUIPromptExtraData = {
|
||||
extra_pnginfo?: ComfyPromptPNGInfo,
|
||||
client_id?: string, // UUID
|
||||
subgraphs: string[],
|
||||
thumbnails?: GalleryOutputEntry[]
|
||||
client_id?: UUID, // UUID
|
||||
}
|
||||
|
||||
type ComfyAPIEvents = {
|
||||
|
||||
@@ -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 ComfyPromptExtraData, type ComfyPromptRequest, type NodeID, type PromptID } from "$lib/api"
|
||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, 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";
|
||||
@@ -349,7 +349,7 @@ export default class ComfyApp {
|
||||
|
||||
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => {
|
||||
this.nodeOutputs[nodeID] = output;
|
||||
const node = this.lGraph.getNodeById(parseInt(nodeID)) as ComfyGraphNode;
|
||||
const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode;
|
||||
if (node?.onExecuted) {
|
||||
node.onExecuted(output);
|
||||
}
|
||||
@@ -467,7 +467,6 @@ export default class ComfyApp {
|
||||
state = structuredClone(blankGraph)
|
||||
}
|
||||
await this.deserialize(state)
|
||||
uiState.update(s => { s.uiUnlocked = true; return s; })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -558,7 +557,7 @@ export default class ComfyApp {
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt(tag: string | null = null): Promise<SerializedPrompt> {
|
||||
graphToPrompt(tag: string | null = null): SerializedPrompt {
|
||||
// Run frontend-only logic
|
||||
this.lGraph.runStep(1)
|
||||
|
||||
@@ -754,14 +753,16 @@ export default class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
const p = await this.graphToPrompt(tag);
|
||||
const p = this.graphToPrompt(tag);
|
||||
const l = layoutState.serialize();
|
||||
console.debug(promptToGraphVis(p))
|
||||
|
||||
const extraData: ComfyPromptExtraData = {
|
||||
const extraData: ComfyBoxPromptExtraData = {
|
||||
extra_pnginfo: {
|
||||
workflow: p.workflow
|
||||
workflow: p.workflow,
|
||||
comfyBoxLayout: l,
|
||||
comfyBoxSubgraphs: [tag],
|
||||
},
|
||||
subgraphs: [tag],
|
||||
thumbnails
|
||||
}
|
||||
|
||||
|
||||
@@ -157,8 +157,10 @@
|
||||
|
||||
let showModal = false;
|
||||
let selectedPrompt = null;
|
||||
let selectedImages = [];
|
||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
||||
selectedPrompt = entry.entry.prompt;
|
||||
selectedImages = entry.images;
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
@@ -178,7 +180,7 @@
|
||||
<h1 style="padding-bottom: 1rem;">Prompt Details</h1>
|
||||
</div>
|
||||
{#if selectedPrompt}
|
||||
<PromptDisplay prompt={selectedPrompt} />
|
||||
<PromptDisplay prompt={selectedPrompt} images={selectedImages} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
@@ -495,6 +497,9 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.queue-remaining {
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<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";
|
||||
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
|
||||
const splitLength = 50;
|
||||
|
||||
export let prompt: SerializedPromptInputsAll;
|
||||
export let images: string[] = [];
|
||||
|
||||
let galleryStyle: Styles = {
|
||||
grid_cols: [2],
|
||||
object_fit: "cover",
|
||||
}
|
||||
|
||||
function isInputLink(input: SerializedPromptInput): boolean {
|
||||
return Array.isArray(input)
|
||||
@@ -55,63 +62,98 @@
|
||||
</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 class="scroll-container">
|
||||
<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 {i+1}: {classType}" open={false}>
|
||||
{#each filtered as [inputName, input]}
|
||||
<Block>
|
||||
<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>
|
||||
{#if images.length > 0}
|
||||
<div class="image-container">
|
||||
<Block>
|
||||
<Gallery
|
||||
value={images}
|
||||
label=""
|
||||
show_label={false}
|
||||
style={galleryStyle}
|
||||
root={""}
|
||||
root_url={""}
|
||||
/>
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.prompt-display {
|
||||
overflow-y: scroll;
|
||||
width: 50vw;
|
||||
width: 70vw;
|
||||
height: 60vh;
|
||||
color: none;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: auto;
|
||||
|
||||
flex-direction: column;
|
||||
@media (min-width: 1600px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
position: relative;
|
||||
/* overflow-y: auto; */
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
flex: 1 1 0%;
|
||||
max-width: 30vw;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
@@ -530,7 +530,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
for (const [nodeId, newMode] of Object.entries(nodeChanges)) {
|
||||
this.graph.getNodeById(parseInt(nodeId)).changeMode(newMode);
|
||||
this.graph.getNodeById(nodeId).changeMode(newMode);
|
||||
}
|
||||
|
||||
const layout = get(layoutState);
|
||||
|
||||
@@ -47,7 +47,7 @@ function notifyToast(text: string, options: NotifyOptions) {
|
||||
}
|
||||
else if (options.type === "warning") {
|
||||
toastOptions.theme = {
|
||||
'--toastBackground': 'var(--color-yellow-500)',
|
||||
'--toastBackground': 'var(--color-yellow-600)',
|
||||
}
|
||||
}
|
||||
else if (options.type === "error") {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type ComfyApp from "$lib/components/ComfyApp"
|
||||
import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions } from "@litegraph-ts/core"
|
||||
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions, type UUID } from "@litegraph-ts/core"
|
||||
import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
import type { ComfyWidgetNode } from '$lib/nodes';
|
||||
import type { NodeID } from '$lib/api';
|
||||
|
||||
type DragItemEntry = {
|
||||
/*
|
||||
@@ -58,12 +59,12 @@ export type LayoutState = {
|
||||
* Items indexed by the litegraph node they're bound to
|
||||
* Only contains drag items of type "widget"
|
||||
*/
|
||||
allItemsByNode: Record<number, DragItemEntry>,
|
||||
allItemsByNode: Record<NodeID, DragItemEntry>,
|
||||
|
||||
/*
|
||||
* Next ID to use for instantiating a new drag item
|
||||
*/
|
||||
currentId: number,
|
||||
currentId: UUID,
|
||||
|
||||
/*
|
||||
* Selected drag items.
|
||||
@@ -642,7 +643,7 @@ export interface WidgetLayout extends IDragItem {
|
||||
node: ComfyWidgetNode
|
||||
}
|
||||
|
||||
export type DragItemID = string;
|
||||
export type DragItemID = UUID;
|
||||
|
||||
type LayoutStateOps = {
|
||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
|
||||
@@ -654,8 +655,8 @@ type LayoutStateOps = {
|
||||
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
|
||||
ungroup: (container: ContainerLayout) => void,
|
||||
getCurrentSelection: () => IDragItem[],
|
||||
findLayoutEntryForNode: (nodeId: number) => DragItemEntry | null,
|
||||
findLayoutForNode: (nodeId: number) => IDragItem | null,
|
||||
findLayoutEntryForNode: (nodeId: NodeID) => DragItemEntry | null,
|
||||
findLayoutForNode: (nodeId: NodeID) => IDragItem | null,
|
||||
serialize: () => SerializedLayoutState,
|
||||
deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
|
||||
initDefaultLayout: () => void,
|
||||
@@ -916,7 +917,7 @@ function ungroup(container: ContainerLayout) {
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function findLayoutEntryForNode(nodeId: number): DragItemEntry | null {
|
||||
function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null {
|
||||
const state = get(store)
|
||||
const found = Object.entries(state.allItems).find(pair =>
|
||||
pair[1].dragItem.type === "widget"
|
||||
@@ -926,7 +927,7 @@ function findLayoutEntryForNode(nodeId: number): DragItemEntry | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLayoutForNode(nodeId: number): WidgetLayout | null {
|
||||
function findLayoutForNode(nodeId: NodeID): WidgetLayout | null {
|
||||
const found = findLayoutEntryForNode(nodeId);
|
||||
if (!found)
|
||||
return null;
|
||||
@@ -963,7 +964,7 @@ function initDefaultLayout() {
|
||||
export type SerializedLayoutState = {
|
||||
root: DragItemID | null,
|
||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||
currentId: number,
|
||||
currentId: UUID,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
@@ -976,7 +977,7 @@ export type SerializedDragEntry = {
|
||||
export type SerializedDragItem = {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
nodeId: number | null,
|
||||
nodeId: UUID | null,
|
||||
attrs: Attributes
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyPromptExtraData, NodeID, PromptID } from "$lib/api";
|
||||
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, NodeID, PromptID } from "$lib/api";
|
||||
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
||||
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import notify from "$lib/notify";
|
||||
@@ -29,10 +29,11 @@ export type QueueEntry = {
|
||||
finishedAt?: Date,
|
||||
promptID: PromptID,
|
||||
prompt: SerializedPromptInputsAll,
|
||||
extraData: ComfyPromptExtraData,
|
||||
extraData: ComfyBoxPromptExtraData,
|
||||
goodOutputs: NodeID[],
|
||||
|
||||
/* Data not sent by Comfy's API, lost on page refresh */
|
||||
/* Data not sent by ComfyUI's API, lost on page refresh */
|
||||
|
||||
/* Prompt outputs, collected while the prompt is still executing */
|
||||
outputs: SerializedPromptOutputs,
|
||||
|
||||
@@ -53,7 +54,7 @@ export type QueueState = {
|
||||
queuePending: Writable<QueueEntry[]>,
|
||||
queueCompleted: Writable<CompletedQueueEntry[]>,
|
||||
queueRemaining: number | "X" | null;
|
||||
runningNodeID: number | null;
|
||||
runningNodeID: NodeID | null;
|
||||
progress: Progress | null,
|
||||
isInterrupting: boolean
|
||||
}
|
||||
@@ -170,7 +171,7 @@ function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) {
|
||||
if (entry != null) {
|
||||
entry.nodesRan.add(runningNodeID)
|
||||
}
|
||||
s.runningNodeID = parseInt(runningNodeID);
|
||||
s.runningNodeID = runningNodeID;
|
||||
}
|
||||
else {
|
||||
// Prompt finished executing.
|
||||
|
||||
@@ -106,7 +106,7 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
return out
|
||||
}
|
||||
|
||||
export function getNodeInfo(nodeId: number): string {
|
||||
export function getNodeInfo(nodeId: NodeID): string {
|
||||
let app = (window as any).app;
|
||||
if (!app || !app.lGraph)
|
||||
return String(nodeId);
|
||||
|
||||
Reference in New Issue
Block a user