diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index c3b792e..72a7107 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -1,14 +1,15 @@ -import { BuiltInSlotShape, LGraphCanvas, LGraphNode, LLink, LiteGraph, NodeMode, Subgraph, TitleMode, type ContextMenuItem, type IContextMenuItem, type NodeID, type Vector2, type Vector4, type MouseEventExt, ContextMenu, type SerializedLGraphNode } from "@litegraph-ts/core"; +import { BuiltInSlotShape, ContextMenu, LGraphCanvas, LGraphNode, LLink, LiteGraph, NodeMode, Subgraph, TitleMode, type ContextMenuItem, type IContextMenuItem, type MouseEventExt, type NodeID, type Vector2, type Vector4, LGraph, type SlotIndex, type SlotNameOrIndex } from "@litegraph-ts/core"; import { get, type Unsubscriber } from "svelte/store"; +import { createTemplate, serializeTemplate, type ComfyBoxTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate"; import type ComfyGraph from "./ComfyGraph"; +import type { ComfyGraphErrorLocation, ComfyGraphErrors, ComfyNodeErrors } from "./apiErrors"; import type ComfyApp from "./components/ComfyApp"; import { ComfyReroute } from "./nodes"; +import notify from "./notify"; import layoutStates, { type ContainerLayout } from "./stores/layoutStates"; import queueState from "./stores/queueState"; import selectionState from "./stores/selectionState"; import templateState from "./stores/templateState"; -import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate"; -import notify from "./notify"; import { calcNodesBoundingBox } from "./utils"; export type SerializedGraphCanvasState = { @@ -20,11 +21,22 @@ export default class ComfyGraphCanvas extends LGraphCanvas { app: ComfyApp | null; private _unsubscribe: Unsubscriber; isExportingSVG: boolean = false; + activeErrors?: ComfyGraphErrors = null; + blinkError: ComfyGraphErrorLocation | null = null; + blinkErrorTime: number = 0; + highlightNodeAndInput: [LGraphNode, number] | null = null; get comfyGraph(): ComfyGraph | null { return this.graph as ComfyGraph; } + clearErrors() { + this.activeErrors = null; + this.blinkError = null; + this.blinkErrorTime = 0; + this.highlightNodeAndInput = null; + } + constructor( app: ComfyApp, canvas: HTMLCanvasElement | string, @@ -90,10 +102,18 @@ export default class ComfyGraphCanvas extends LGraphCanvas { let state = get(queueState); let ss = get(selectionState); - const isRunningNode = node.id === state.runningNodeID + + const isRunningNode = node.id == state.runningNodeID + const nodeErrors = this.activeErrors?.errorsByID[node.id]; + const isHighlightedNode = this.highlightNodeAndInput && this.highlightNodeAndInput[0].id === node.id; + + if (this.blinkErrorTime > 0) { + this.blinkErrorTime -= this.graph.elapsed_time; + } let color = null; let thickness = 1; + let blink = false; // if (this._selectedNodes.has(node.id)) { // color = "yellow"; // thickness = 5; @@ -104,6 +124,29 @@ export default class ComfyGraphCanvas extends LGraphCanvas { else if (isRunningNode) { color = "#0f0"; } + else if (nodeErrors) { + const hasExecutionError = nodeErrors.find(e => e.errorType === "execution"); + if (hasExecutionError) { + blink = true; + color = "#f0f"; + } + else { + color = "red"; + } + thickness = 2 + } + else if (isHighlightedNode) { + color = "cyan"; + thickness = 2 + } + + if (blink) { + if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) { + if ((Math.floor(this.blinkErrorTime / 2)) % 2 === 0) { + color = null; + } + } + } if (color) { this.drawNodeOutline(node, ctx, size, mouseOver, fgColor, bgColor, color, thickness) @@ -114,6 +157,54 @@ export default class ComfyGraphCanvas extends LGraphCanvas { ctx.fillRect(0, 0, size[0] * (state.progress.value / state.progress.max), 6); ctx.fillStyle = bgColor; } + + if (nodeErrors) { + this.drawFailedValidationInputs(node, nodeErrors, color, ctx); + } + + if (isHighlightedNode) { + let draw = true; + if (this.blinkErrorTime > 0) { + if ((Math.floor(this.blinkErrorTime / 2)) % 2 === 0) { + draw = false; + } + } + if (draw) { + const [node, inputSlot] = this.highlightNodeAndInput; + ctx.lineWidth = 2; + ctx.strokeStyle = color; + this.highlightNodeInput(node, inputSlot, ctx); + } + } + } + + private drawFailedValidationInputs(node: LGraphNode, errors: ComfyGraphErrorLocation[], color: string, ctx: CanvasRenderingContext2D) { + ctx.lineWidth = 2; + ctx.strokeStyle = color || "red"; + for (const errorLocation of errors) { + if (errorLocation.input != null) { + if (errorLocation === this.blinkError && this.blinkErrorTime > 0) { + if ((Math.floor(this.blinkErrorTime / 2)) % 2 === 0) { + continue; + } + } + this.highlightNodeInput(node, errorLocation.input.name, ctx); + } + } + } + + private highlightNodeInput(node: LGraphNode, inputSlot: SlotNameOrIndex, ctx: CanvasRenderingContext2D) { + let inputIndex: number; + if (typeof inputSlot === "number") + inputIndex = inputSlot + else + inputIndex = node.findInputSlotIndexByName(inputSlot) + if (inputIndex !== -1) { + let pos = node.getConnectionPos(true, inputIndex); + ctx.beginPath(); + ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) + ctx.stroke(); + } } private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Vector2, mouseOver: boolean, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) { @@ -568,4 +659,61 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } return false; } + + jumpToFirstError() { + this.jumpToError(0); + } + + jumpToError(index: number | ComfyGraphErrorLocation) { + if (this.activeErrors == null) { + return; + } + + let error; + if (typeof index === "number") { + error = this.activeErrors.errors[index] + } + else { + error = index; + } + + if (error == null) { + return; + } + + const rootGraph = this.graph.getRootGraph() + if (rootGraph == null) { + return + } + + const node = rootGraph.getNodeByIdRecursive(error.nodeID); + if (node == null) { + notify(`Couldn't find node '${error.comfyNodeType}' (${error.nodeID})`, { type: "warning" }) + return + } + + this.jumpToNode(node) + + this.highlightNodeAndInput = null; + this.blinkError = error; + this.blinkErrorTime = 20; + } + + jumpToNode(node: LGraphNode) { + this.closeAllSubgraphs(); + + const subgraphs = Array.from(node.iterateParentSubgraphNodes()).reverse(); + + for (const subgraph of subgraphs) { + this.openSubgraph(subgraph.subgraph) + } + + this.centerOnNode(node); + } + + jumpToNodeAndInput(node: LGraphNode, slotIndex: number) { + this.jumpToNode(node); + this.highlightNodeAndInput = [node, slotIndex]; + this.blinkErrorTime = 20; + } } diff --git a/src/lib/api.ts b/src/lib/api.ts index 708968b..7d7f47d 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,11 +1,12 @@ -import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs, SerializedAppState } from "./components/ComfyApp"; +import type { Progress, SerializedPrompt, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptOutputs, SerializedAppState, SerializedPromptInput, SerializedPromptInputLink } from "./components/ComfyApp"; import type TypedEmitter from "typed-emitter"; import EventEmitter from "events"; import type { ComfyImageLocation } from "$lib/utils"; import type { SerializedLGraph, UUID } from "@litegraph-ts/core"; import type { SerializedLayoutState } from "./stores/layoutStates"; -import type { ComfyNodeDef } from "./ComfyNodeDef"; +import type { ComfyNodeDef, ComfyNodeDefInput } from "./ComfyNodeDef"; import type { WorkflowInstID } from "./stores/workflowState"; +import type { ComfyAPIPromptErrorResponse } from "./apiErrors"; export type ComfyPromptRequest = { client_id?: string, @@ -43,11 +44,12 @@ export type ComfyAPIHistoryItem = [ ComfyNodeID[] // good outputs ] -export type ComfyAPIPromptResponse = { - promptID?: PromptID, - error?: string +export type ComfyAPIPromptSuccessResponse = { + promptID: PromptID } +export type ComfyAPIPromptResponse = ComfyAPIPromptSuccessResponse | ComfyAPIPromptErrorResponse + export type ComfyAPIHistoryEntry = { prompt: ComfyAPIHistoryItem, outputs: SerializedPromptOutputs @@ -92,7 +94,8 @@ type ComfyAPIEvents = { executed: (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutputs) => void, execution_start: (promptID: PromptID) => void, execution_cached: (promptID: PromptID, nodes: ComfyNodeID[]) => void, - execution_error: (promptID: PromptID, message: string) => void, + execution_interrupted: (error: ComfyInterruptedError) => void, + execution_error: (error: ComfyExecutionError) => void, } export default class ComfyAPI { @@ -201,8 +204,11 @@ export default class ComfyAPI { case "execution_cached": this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes); break; + case "execution_interrupted": + this.eventBus.emit("execution_interrupted", msg.data); + break; case "execution_error": - this.eventBus.emit("execution_error", msg.data.prompt_id, msg.data.message); + this.eventBus.emit("execution_error", msg.data); break; default: console.warn("Unhandled message:", event.data); diff --git a/src/lib/apiErrors.ts b/src/lib/apiErrors.ts new file mode 100644 index 0000000..e631cbc --- /dev/null +++ b/src/lib/apiErrors.ts @@ -0,0 +1,270 @@ +import type { NodeID } from "@litegraph-ts/core" +import type { ComfyNodeDefInput } from "./ComfyNodeDef" +import type { ComfyNodeID, PromptID } from "./api" +import type { SerializedPromptInputLink } from "./components/ComfyApp" +import type { WorkflowError, WorkflowInstID } from "./stores/workflowState" +import { exclude_internal_props } from "svelte/internal" +import type ComfyGraphCanvas from "./ComfyGraphCanvas" +import type { QueueEntry } from "./stores/queueState" + +enum ComfyPromptErrorType { + NoOutputs = "prompt_no_outputs", + OutputsFailedValidation = "prompt_outputs_failed_validation", +} + +export interface ComfyPromptError { + type: ComfyPromptErrorType, + message: string, + details: string, + extra_info: T +} + +export interface CPENoOutputs extends ComfyPromptError { + type: ComfyPromptErrorType.NoOutputs +} + +export interface CPEOutputsFailedValidation extends ComfyPromptError { + type: ComfyPromptErrorType.OutputsFailedValidation +} + +export enum ComfyNodeErrorType { + RequiredInputMissing = "required_input_missing", + BadLinkedInput = "bad_linked_input", + ReturnTypeMismatch = "return_type_mismatch", + InvalidInputType = "invalid_input_type", + ValueSmallerThanMin = "value_smaller_than_min", + ValueBiggerThanMax = "value_bigger_than_max", + CustomValidationFailed = "custom_validation_failed", + ValueNotInList = "value_not_in_list", + ExceptionDuringValidation = "exception_during_validation", + ExceptionDuringInnerValidation = "exception_during_inner_validation", +} + +export interface ComfyNodeError { + type: ComfyNodeErrorType, + message: string, + details: string, + extra_info: T +} + +export type ComfyNodeErrors = { + errors: ComfyNodeError[], + dependent_outputs: ComfyNodeID[], + class_type: string +} + +export type InputWithValue = { + input_name: string, + input_config: ComfyNodeDefInput, + received_value: any +} + +function isInputWithValue(param: any): param is InputWithValue { + return param && "input_name" in param; +} + +export type InputWithValueAndException = InputWithValue & { + exception_message: string +} + +export type InputWithLinkedNode = { + input_name: string, + input_config: ComfyNodeDefInput, + linked_node: SerializedPromptInputLink +} + +export type ValidationException = { + exception_type: string, + traceback: string[] +} + +function isValidationException(param: any): param is ValidationException { + return param && "exception_type" in param && "traceback" in param; +} + +export interface CNERequiredInputMissing extends ComfyNodeError<{ input_name: string }> { + type: ComfyNodeErrorType.RequiredInputMissing +} + +export interface CNEBadLinkedInput extends ComfyNodeError { + type: ComfyNodeErrorType.BadLinkedInput +} + +export interface CNEReturnTypeMismatch extends ComfyNodeError { + type: ComfyNodeErrorType.ReturnTypeMismatch +} + +export interface CNEInvalidInputType extends ComfyNodeError { + type: ComfyNodeErrorType.InvalidInputType +} + +export interface CNEValueSmallerThanMin extends ComfyNodeError { + type: ComfyNodeErrorType.ValueSmallerThanMin +} + +export interface CNEValueBiggerThanMax extends ComfyNodeError { + type: ComfyNodeErrorType.ValueBiggerThanMax +} + +export interface CNECustomValidationFailed extends ComfyNodeError { + type: ComfyNodeErrorType.CustomValidationFailed +} + +export interface CNEValueNotInList extends ComfyNodeError { + type: ComfyNodeErrorType.ValueNotInList +} + +export interface CNEExceptionDuringValidation extends ComfyNodeError { + type: ComfyNodeErrorType.ExceptionDuringValidation +} + +export interface CNEExceptionDuringInnerValidation extends ComfyNodeError { + type: ComfyNodeErrorType.ExceptionDuringInnerValidation +} + +export type ComfyAPIPromptErrorResponse = { + error: ComfyPromptError, + node_errors: Record, +} + +export type ComfyInterruptedError = { + prompt_id: PromptID, + node_id: ComfyNodeID, + node_type: string, + executed: ComfyNodeID[] +} + +export type ComfyExecutionError = ComfyInterruptedError & { + exception_message: string, + exception_type: string, + traceback: string[], + current_inputs: any[], + current_outputs: any[][], +} + +export function formatValidationError(error: ComfyAPIPromptErrorResponse) { + return `${error.error.message}: ${error.error.details}` +} + +export function formatExecutionError(error: ComfyExecutionError) { + return error.exception_message +} + +export type ComfyGraphErrorInput = { + name: string, + config?: ComfyNodeDefInput, + + receivedValue?: any, + receivedType?: string, + linkedNode?: SerializedPromptInputLink +} + +export type ComfyGraphErrorLocation = { + workflowID: WorkflowInstID, + nodeID: NodeID, + comfyNodeType: string, + errorType: ComfyNodeErrorType | "execution", + message: string, + dependentOutputs: NodeID[], + queueEntry: QueueEntry, + + input?: ComfyGraphErrorInput, + + exceptionMessage?: string, + exceptionType?: string, + traceback?: string[], + + inputValues?: any[], + outputValues?: any[][], +} + +export type ComfyGraphErrors = { + message: string, + errors: ComfyGraphErrorLocation[], + errorsByID: Record +} + +export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validationError: ComfyAPIPromptErrorResponse, queueEntry: QueueEntry): ComfyGraphErrors { + const errorsByID: Record = {} + + for (const [nodeID, nodeErrors] of Object.entries(validationError.node_errors)) { + errorsByID[nodeID] = nodeErrors.errors.map(e => { + const loc: ComfyGraphErrorLocation = { + workflowID, + nodeID, + comfyNodeType: nodeErrors.class_type, + errorType: e.type, + message: e.message, + dependentOutputs: nodeErrors.dependent_outputs, + queueEntry + } + + if (isInputWithValue(e.extra_info)) { + loc.input = { + name: e.extra_info.input_name, + config: e.extra_info.input_config, + receivedValue: e.extra_info.received_value + } + + if ("received_type" in e.extra_info) { + loc.input.receivedType = e.extra_info.received_type as string; + } + if ("linked_node" in e.extra_info) { + loc.input.linkedNode = e.extra_info.linked_node as SerializedPromptInputLink; + } + } + + if ("exception_message" in e.extra_info) { + loc.exceptionMessage = e.extra_info.exception_message as "string" + } + + if (isValidationException(e.extra_info)) { + loc.exceptionType = e.extra_info.exception_type; + loc.traceback = e.extra_info.traceback; + } + + return loc; + }) + } + + return { + message: validationError.error.message, + errors: Object.values(errorsByID).flatMap(e => e), + errorsByID + } +} + +export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executionError: ComfyExecutionError, queueEntry: QueueEntry): ComfyGraphErrors { + const errorsByID: Record = {} + + errorsByID[executionError.node_id] = [{ + workflowID, + nodeID: executionError.node_id, + comfyNodeType: executionError.node_type, + errorType: "execution", + message: executionError.exception_message, + dependentOutputs: [], // TODO + queueEntry, + + exceptionMessage: executionError.exception_message, + exceptionType: executionError.exception_type, + traceback: executionError.traceback, + inputValues: executionError.current_inputs, + outputValues: executionError.current_outputs, + }] + + return { + message: executionError.exception_message, + errors: Object.values(errorsByID).flatMap(e => e), + errorsByID + } +} + +export function workflowErrorToGraphErrors(workflowID: WorkflowInstID, workflowError: WorkflowError, queueEntry: QueueEntry): ComfyGraphErrors { + if (workflowError.type === "validation") { + return validationErrorToGraphErrors(workflowID, workflowError.error, queueEntry) + } + else { + return executionErrorToGraphErrors(workflowID, workflowError.error, queueEntry) + } +} diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 551abac..9f905be 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1,4 +1,4 @@ -import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyNodeID, type ComfyPromptRequest, type PromptID, type QueueItemType } from "$lib/api"; +import ComfyAPI, { type iomfyAPIPromptResponse, type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyNodeID, type ComfyPromptRequest, type PromptID, type QueueItemType } from "$lib/api"; import { parsePNGMetadata } from "$lib/pnginfo"; import { BuiltInSlotType, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type INodeInputSlot, type LGraphNodeConstructor, type NodeID, type NodeTypeOpts, type SerializedLGraph, type SlotIndex } from "@litegraph-ts/core"; import A1111PromptModal from "./modal/A1111PromptModal.svelte"; @@ -38,6 +38,7 @@ import ComfyPromptSerializer, { isActiveBackendNode, nodeHasTag, UpstreamNodeLoc import DanbooruTags from "$lib/DanbooruTags"; import { deserializeTemplateFromSVG, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate"; import templateState from "$lib/stores/templateState"; +import { formatValidationError, type ComfyAPIPromptErrorResponse, formatExecutionError, type ComfyExecutionError } from "$lib/apiErrors"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -91,7 +92,8 @@ export type SerializedAppState = { } /** [link_origin, link_slot_index] | input_value */ -export type SerializedPromptInput = [ComfyNodeID, number] | any +export type SerializedPromptInputLink = [ComfyNodeID, number] +export type SerializedPromptInput = SerializedPromptInputLink | any export type SerializedPromptInputs = Record; @@ -307,7 +309,7 @@ export default class ComfyApp { if (errors && errors.length > 0) error = "Error(s) loading builtin templates:\n" + errors.join("\n"); - console.log(`Loaded {templates.length} builtin templates.`); + console.log(`Loaded ${templates.length} builtin templates.`); return [templates, error] }) @@ -597,9 +599,33 @@ export default class ComfyApp { queueState.executionCached(promptID, nodes) }); - this.api.addEventListener("execution_error", (promptID: PromptID, message: string) => { - queueState.executionError(promptID, message) - notify(`Execution error: ${message}`, { type: "error", timeout: 10000 }) + this.api.addEventListener("execution_error", (error: ComfyExecutionError) => { + const completedEntry = queueState.executionError(error) + let workflow: ComfyBoxWorkflow | null; + if (completedEntry) { + const workflowID = completedEntry.entry.extraData.workflowID; + if (workflowID) { + workflow = workflowState.getWorkflow(workflowID) + } + } + + if (workflow) { + workflowState.executionError(workflow.id, error.prompt_id) + notify( + `Execution error in workflow "${workflow.attrs.title}".\nClick for details.`, + { + type: "error", + showBar: true, + timeout: 15 * 1000, + onClick: () => { + uiState.update(s => { s.activeError = error.prompt_id; return s }) + } + }) + } + else { + const message = formatExecutionError(error); + notify(`Execution error: ${message}`, { type: "error", timeout: 10000 }) + } }); this.api.init(); @@ -1006,8 +1032,9 @@ export default class ComfyApp { thumbnails } - let error: string | null = null; - let promptID: PromptID | null = null; + let error: ComfyAPIPromptErrorResponse | null = null; + let errorMes: string | null = null; + let errorPromptID: PromptID | null = null; const request: ComfyPromptRequest = { number: num, @@ -1017,22 +1044,36 @@ export default class ComfyApp { try { const response = await this.api.queuePrompt(request); - if (response.error != null) { + if ("error" in response) { error = response; + errorMes = formatValidationError(error) + errorPromptID = queueState.promptError(workflow.id, response, p, extraData) + workflowState.promptError(workflow.id, errorPromptID) } else { queueState.afterQueued(workflow.id, response.promptID, num, p.output, extraData) + workflowState.afterQueued(workflow.id, response.promptID, p, extraData) } } catch (err) { - error = err?.toString(); + errorMes = err?.toString(); } if (error != null) { - const mes: any = error; - notify(`Error queuing prompt: \n${mes} `, { type: "error" }) + notify( + `Prompt validation failed.\nClick for details.`, + { + type: "error", + showBar: true, + timeout: 1000 * 15, + onClick: () => { + uiState.update(s => { s.activeError = errorPromptID; return s }) + } + }) console.error(graphToGraphVis(workflow.graph)) console.error(promptToGraphVis(p)) console.error("Error queuing prompt", error, num, p) + } + else if (errorMes != null) { break; } @@ -1254,7 +1295,7 @@ export default class ComfyApp { let defaultValue = null; if (foundInput != null) { const comfyInput = foundInput as IComfyInputSlot; - console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values) + console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values.length) values = comfyInput.config.values; defaultValue = comfyInput.config.defaultValue; } diff --git a/src/lib/components/ComfyBoxWorkflowsView.svelte b/src/lib/components/ComfyBoxWorkflowsView.svelte index 8cc5aa1..816dc39 100644 --- a/src/lib/components/ComfyBoxWorkflowsView.svelte +++ b/src/lib/components/ComfyBoxWorkflowsView.svelte @@ -1,3 +1,7 @@ + +
diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte new file mode 100644 index 0000000..ad68d26 --- /dev/null +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -0,0 +1,299 @@ + + +
+
+ +
+ {#each Object.entries(errors.errorsByID) as [nodeID, nodeErrors]} + {@const first = nodeErrors[0]} + {@const parent = getParentNode(first)} +
+
+ {first.comfyNodeType} + {#if parent} + ({parent.title}) + {/if} +
+
+ {#each nodeErrors as error} + {@const isExecutionError = error.errorType === "execution"} +
+
+
+ +
+ {error.message} + {#if error.exceptionType} + ({error.exceptionType}) + {/if} + {#if error.exceptionMessage && !isExecutionError} +
{error.exceptionMessage}
+ {/if} + {#if error.input} +
+ Input: {error.input.name} + {#if error.input.config} + ({getInputTypeName(error.input.config[0])}) + {/if} +
+ {#if canJumpToDisconnectedInput(error)} +
+ + Find disconnected input +
+ {/if} + + {#if error.input.receivedValue} +
+ Received value: {error.input.receivedValue} +
+ {/if} + {#if error.input.receivedType} +
+ Received type: {error.input.receivedType} +
+ {/if} + {#if error.input.config} +
+ +
+
+ +
+
+
+
+ {/if} + {/if} +
+
+
+ {#if error.traceback} +
+ +
+
+ {#each error.traceback as line} +
{line}
+ {/each} +
+
+
+
+ {/if} +
+ {/each} +
+
+ {/each} +
+ + diff --git a/src/lib/components/ComfyGraphView.svelte b/src/lib/components/ComfyGraphView.svelte index 44b35ac..6dea59b 100644 --- a/src/lib/components/ComfyGraphView.svelte +++ b/src/lib/components/ComfyGraphView.svelte @@ -1,28 +1,48 @@
- +
- {#if !$interfaceState.graphTransitioning} - - - - {/if} + + + {#if $uiState.activeError != null} + + {/if} +
+ {#if $uiState.activeError && app?.lCanvas?.activeErrors != null} + + {/if}
diff --git a/src/lib/components/ComfyPromptSerializer.ts b/src/lib/components/ComfyPromptSerializer.ts index 1e1a540..51808d4 100644 --- a/src/lib/components/ComfyPromptSerializer.ts +++ b/src/lib/components/ComfyPromptSerializer.ts @@ -71,11 +71,13 @@ export function isActiveBackendNode(node: LGraphNode, tag: string | null = null) return true; } +type UpstreamResult = [LGraph | null, LLink | null, number | null, LGraphNode | null]; + export class UpstreamNodeLocator { constructor(private isTheTargetNode: (node: LGraphNode) => boolean) { } - private followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink | null] { + private followSubgraph(subgraph: Subgraph, link: LLink): UpstreamResult { if (link.origin_id != subgraph.id) throw new Error("Invalid link and graph output!") @@ -84,10 +86,10 @@ export class UpstreamNodeLocator { throw new Error("No inner graph input!") const nextLink = innerGraphOutput.getInputLink(0) - return [innerGraphOutput.graph, nextLink]; + return [innerGraphOutput.graph, nextLink, 0, innerGraphOutput]; } - private followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null, LLink | null] { + private followGraphInput(graphInput: GraphInput, link: LLink): UpstreamResult { if (link.origin_id != graphInput.id) throw new Error("Invalid link and graph input!") @@ -100,10 +102,10 @@ export class UpstreamNodeLocator { throw new Error("No outer input slot!") const nextLink = outerSubgraph.getInputLink(outerInputIndex) - return [outerSubgraph.graph, nextLink]; + return [outerSubgraph.graph, nextLink, outerInputIndex, outerSubgraph]; } - private getUpstreamLink(parent: LGraphNode, currentLink: LLink): [LGraph | null, LLink | null] { + private getUpstreamLink(parent: LGraphNode, currentLink: LLink): UpstreamResult { if (parent.is(Subgraph)) { console.debug("FollowSubgraph") return this.followSubgraph(parent, currentLink); @@ -113,17 +115,18 @@ export class UpstreamNodeLocator { return this.followGraphInput(parent, currentLink); } else if ("getUpstreamLink" in parent) { - return [parent.graph, (parent as ComfyGraphNode).getUpstreamLink()]; + const link = (parent as ComfyGraphNode).getUpstreamLink(); + return [parent.graph, link, link?.target_slot, parent]; } else if (parent.inputs.length === 1) { // Only one input, so assume we can follow it backwards. const link = parent.getInputLink(0); if (link) { - return [parent.graph, link] + return [parent.graph, link, 0, parent] } } console.warn("[graphToPrompt] Frontend node does not support getUpstreamLink", parent.type) - return [null, null]; + return [null, null, null, null]; } /* @@ -133,13 +136,15 @@ export class UpstreamNodeLocator { * Returns the node and the output link attached to it that leads to the * starting node if any. */ - locateUpstream(fromNode: LGraphNode, inputIndex: SlotIndex, tag: string | null): [LGraphNode | null, LLink | null] { + locateUpstream(fromNode: LGraphNode, inputIndex: SlotIndex, tag: string | null): [LGraphNode | null, LLink | null, number | null, LGraphNode | null] { let parent = fromNode.getInputNode(inputIndex); if (!parent) - return [null, null]; + return [null, null, null, null]; const seen = {} let currentLink = fromNode.getInputLink(inputIndex); + let currentInputSlot = inputIndex; + let currentNode = fromNode; const shouldFollowParent = (parent: LGraphNode) => { return isActiveNode(parent, tag) && !this.isTheTargetNode(parent); @@ -152,7 +157,10 @@ export class UpstreamNodeLocator { // nodes have conditional logic that determines which link // to follow backwards. while (shouldFollowParent(parent)) { - const [nextGraph, nextLink] = this.getUpstreamLink(parent, currentLink); + const [nextGraph, nextLink, nextInputSlot, nextNode] = this.getUpstreamLink(parent, currentLink); + + currentInputSlot = nextInputSlot; + currentNode = nextNode; if (nextLink == null) { console.warn("[graphToPrompt] No upstream link found in frontend node", parent) @@ -176,9 +184,9 @@ export class UpstreamNodeLocator { } if (!isActiveNode(parent, tag) || !this.isTheTargetNode(parent) || currentLink == null) - return [null, null]; + return [null, currentLink, currentInputSlot, currentNode]; - return [parent, currentLink] + return [parent, currentLink, currentInputSlot, currentNode] } } diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index 3b66d04..9d8bd75 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -8,7 +8,8 @@ date?: string, status: QueueUIEntryStatus, images?: string[], // URLs - details?: string // shown in a tooltip on hover + details?: string, // shown in a tooltip on hover + error?: WorkflowError } @@ -21,15 +22,14 @@ import { convertComfyOutputToComfyURL, convertFilenameToComfyURL, getNodeInfo, truncateString } from "$lib/utils" import type { Writable } from "svelte/store"; import type { QueueItemType } from "$lib/api"; - import { ImageViewer } from "$lib/ImageViewer"; import { Button } from "@gradio/button"; import type ComfyApp from "./ComfyApp"; - import { tick } from "svelte"; + import { getContext, tick } from "svelte"; import Modal from "./Modal.svelte"; - import DropZone from "./DropZone.svelte"; - import workflowState from "$lib/stores/workflowState"; + import { type WorkflowError } from "$lib/stores/workflowState"; import ComfyQueueListDisplay from "./ComfyQueueListDisplay.svelte"; import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte"; + import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte"; export let app: ComfyApp; @@ -38,6 +38,8 @@ let queueCompleted: Writable | null = null; let queueList: HTMLDivElement | null = null; + const { showError } = getContext(WORKFLOWS_VIEW) as any; + $: if ($queueState) { queuePending = $queueState.queuePending queueRunning = $queueState.queueRunning @@ -144,9 +146,11 @@ if (entry.extraData?.workflowTitle != null) { message = `${entry.extraData.workflowTitle}` } - const subgraphsString = subgraphs.join(', ') - if (subgraphsString.length > 0) - message += ` (${subgraphsString})` + if (subgraphs) { + const subgraphsString = subgraphs.join(', ') + if (subgraphsString.length > 0) + message += ` (${subgraphsString})` + } let submessage = `Nodes: ${Object.keys(entry.prompt).length}` @@ -198,7 +202,7 @@ else if (entry.status === "interrupted" || entry.status === "all_cached") result.submessage = "Prompt was interrupted." if (entry.error) - result.details = entry.error + result.error = entry.error return result; } @@ -232,10 +236,20 @@ let selectedPrompt = null; let selectedImages = []; function showPrompt(entry: QueueUIEntry) { - selectedPrompt = entry.entry.prompt; - selectedImages = entry.images; - showModal = true; - expandAll = false + if (entry.error != null) { + showModal = false; + expandAll = false; + selectedPrompt = null; + selectedImages = []; + + showError(entry.entry.promptID); + } + else { + selectedPrompt = entry.entry.prompt; + selectedImages = entry.images; + showModal = true; + expandAll = false + } } function closeModal() { diff --git a/src/lib/components/ComfyQueueListDisplay.svelte b/src/lib/components/ComfyQueueListDisplay.svelte index 09caf2e..751a9f8 100644 --- a/src/lib/components/ComfyQueueListDisplay.svelte +++ b/src/lib/components/ComfyQueueListDisplay.svelte @@ -148,8 +148,11 @@ &.success { /* background: green; */ } + &.validation_failed { + background: #551a1a; + } &.error { - background: red; + background: #401a40; } &.all_cached, &.interrupted { filter: brightness(80%); diff --git a/src/lib/components/OnClickToastItem.svelte b/src/lib/components/OnClickToastItem.svelte new file mode 100644 index 0000000..b1d8a11 --- /dev/null +++ b/src/lib/components/OnClickToastItem.svelte @@ -0,0 +1,18 @@ + + + +
{message}
diff --git a/src/lib/components/gradio/image/StaticImage.svelte b/src/lib/components/gradio/image/StaticImage.svelte index eda153d..a060161 100644 --- a/src/lib/components/gradio/image/StaticImage.svelte +++ b/src/lib/components/gradio/image/StaticImage.svelte @@ -2,7 +2,7 @@ import { createEventDispatcher } from "svelte"; import type { SelectData } from "@gradio/utils"; import { BlockLabel, Empty, IconButton } from "@gradio/atoms"; - import { Download } from "@gradio/icons"; + import { Download, Clear } from "@gradio/icons"; import { get_coordinates_of_clicked_image } from "./utils"; import { Image } from "@gradio/icons"; @@ -33,13 +33,17 @@ dispatch("select", { index: coordinates, value: null }); } }; + + function remove() { + value = null; + } {#if value === null} {:else} -
+
+
@@ -63,9 +68,13 @@ cursor: crosshair; } - .download { + .buttons { + display: flex; position: absolute; - top: 6px; - right: 6px; + top: var(--size-2); + right: var(--size-2); + justify-content: flex-end; + gap: var(--spacing-sm); + z-index: var(--layer-5); } diff --git a/src/lib/nodes/widgets/ComfyGalleryNode.ts b/src/lib/nodes/widgets/ComfyGalleryNode.ts index afbe8ff..1267d18 100644 --- a/src/lib/nodes/widgets/ComfyGalleryNode.ts +++ b/src/lib/nodes/widgets/ComfyGalleryNode.ts @@ -99,6 +99,12 @@ export default class ComfyGalleryNode extends ComfyWidgetNode void, } function notifyf7(text: string, options: NotifyOptions) { @@ -19,13 +22,19 @@ function notifyf7(text: string, options: NotifyOptions) { if (closeTimeout === undefined) closeTimeout = 3000; + const on: Notification.Parameters["on"] = {} + if (options.onClick) { + on.click = () => options.onClick(); + } + const notification = f7.notification.create({ title: options.title, titleRightText: 'now', // subtitle: 'Notification with close on click', text: text, closeOnClick: true, - closeTimeout + closeTimeout, + on }); notification.open(); } @@ -33,30 +42,47 @@ function notifyf7(text: string, options: NotifyOptions) { function notifyToast(text: string, options: NotifyOptions) { const toastOptions: SvelteToastOptions = { dismissable: options.timeout !== null, + duration: options.timeout || 5000, + theme: {}, + } + + if (options.showBar) { + toastOptions.theme['--toastBarHeight'] = "6px" } if (options.type === "success") { - toastOptions.theme = { - '--toastBackground': 'var(--color-green-600)', - } + toastOptions.theme['--toastBackground'] = 'var(--color-green-600)'; + toastOptions.theme['--toastBarBackground'] = 'var(--color-green-900)'; } else if (options.type === "info") { - toastOptions.theme = { - '--toastBackground': 'var(--color-blue-500)', - } + toastOptions.theme['--toastBackground'] = 'var(--color-blue-500)'; + toastOptions.theme['--toastBarBackground'] = 'var(--color-blue-800)'; } else if (options.type === "warning") { - toastOptions.theme = { - '--toastBackground': 'var(--color-yellow-600)', - } + toastOptions.theme['--toastBackground'] = 'var(--color-yellow-600)'; + toastOptions.theme['--toastBarBackground'] = 'var(--color-yellow-900)'; } else if (options.type === "error") { - toastOptions.theme = { - '--toastBackground': 'var(--color-red-500)', - } + toastOptions.theme['--toastBackground'] = 'var(--color-red-500)'; + toastOptions.theme['--toastBarBackground'] = 'var(--color-red-800)'; } - toast.push(text, toastOptions); + if (options.onClick) { + toast.push({ + component: { + src: OnClickToastItem, + props: { + message: text, + notifyOptions: options + }, + sendIdTo: "toastID" + }, + ...toastOptions + }) + } + else { + toast.push(text, toastOptions); + } } function notifyNative(text: string, options: NotifyOptions) { @@ -78,7 +104,11 @@ function notifyNative(text: string, options: NotifyOptions) { const notification = new Notification(title, nativeOptions); - notification.onclick = () => window.focus(); + notification.onclick = () => { + window.focus(); + if (options.onClick) + options.onClick(); + } } export default function notify(text: string, options: NotifyOptions = {}) { diff --git a/src/lib/stores/queueState.ts b/src/lib/stores/queueState.ts index 996377f..82712d9 100644 --- a/src/lib/stores/queueState.ts +++ b/src/lib/stores/queueState.ts @@ -1,12 +1,14 @@ -import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID, QueueItemType } from "$lib/api"; -import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs, WorkflowInstID } from "$lib/components/ComfyApp"; +import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyExecutionError, ComfyNodeID, PromptID, QueueItemType } from "$lib/api"; +import type { ComfyAPIPromptErrorResponse } from "$lib/apiErrors"; +import type { Progress, SerializedPrompt, SerializedPromptInputsAll, SerializedPromptOutputs, } from "$lib/components/ComfyApp"; import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes"; import notify from "$lib/notify"; -import { get, writable, type Writable } from "svelte/store"; -import workflowState from "./workflowState"; import { playSound } from "$lib/utils"; +import { get, writable, type Writable } from "svelte/store"; +import { v4 as uuidv4 } from "uuid"; +import workflowState, { type WorkflowError, type WorkflowExecutionError, type WorkflowInstID, type WorkflowValidationError } from "./workflowState"; -export type QueueEntryStatus = "success" | "error" | "interrupted" | "all_cached" | "unknown"; +export type QueueEntryStatus = "success" | "validation_failed" | "error" | "interrupted" | "all_cached" | "unknown"; type QueueStateOps = { queueUpdated: (resp: ComfyAPIQueueResponse) => void, @@ -15,13 +17,14 @@ type QueueStateOps = { executionStart: (promptID: PromptID) => void, executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => QueueEntry | null; executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void, - executionError: (promptID: PromptID, message: string) => void, + executionError: (error: ComfyExecutionError) => CompletedQueueEntry | null, progressUpdated: (progress: Progress) => void getQueueEntry: (promptID: PromptID) => QueueEntry | null; afterQueued: (workflowID: WorkflowInstID, promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void queueItemDeleted: (type: QueueItemType, id: PromptID) => void; queueCleared: (type: QueueItemType) => void; onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => QueueEntry | null + promptError: (id: WorkflowInstID, error: ComfyAPIPromptErrorResponse, prompt: SerializedPrompt, extraData: ComfyBoxPromptExtraData) => PromptID } /* @@ -34,6 +37,10 @@ export type QueueEntry = { number: number, queuedAt?: Date, finishedAt?: Date, + /* + * Can also be generated by the frontend if prompt validation fails + * (the backend won't send back a prompt ID in that case) + */ promptID: PromptID, prompt: SerializedPromptInputsAll, extraData: ComfyBoxPromptExtraData, @@ -46,7 +53,7 @@ export type QueueEntry = { /* Nodes of the workflow that have finished running so far. */ nodesRan: Set, /* Nodes of the workflow the backend reported as cached. */ - cachedNodes: Set + cachedNodes: Set, } /* @@ -61,7 +68,7 @@ export type CompletedQueueEntry = { /** Message to display in the frontend */ message?: string, /** Detailed error/stacktrace, perhaps inspectible with a popup */ - error?: string, + error?: WorkflowError } /* @@ -229,21 +236,32 @@ function moveToRunning(index: number, queue: Writable) { store.set(state) } -function moveToCompleted(index: number, queue: Writable, status: QueueEntryStatus, message?: string, error?: string) { +function moveToCompleted(index: number, queue: Writable, status: QueueEntryStatus, message?: string, error?: ComfyExecutionError): CompletedQueueEntry { const state = get(store) - const entry = get(queue)[index]; + + let workflowError: WorkflowExecutionError | null = null; + if (error) { + workflowError = { + type: "execution", + error + } + entry.nodesRan = new Set(error.executed); + } + console.debug("[queueState] Move to completed", entry.promptID, index, status, message, error) entry.finishedAt = new Date() // Now queue.update(qp => { qp.splice(index, 1); return qp }); + const completed: CompletedQueueEntry = { entry, status, message, error: workflowError } state.queueCompleted.update(qc => { - const completed: CompletedQueueEntry = { entry, status, message, error } qc.push(completed) return qc }) state.isInterrupting = false; store.set(state) + + return completed; } function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null): QueueEntry | null { @@ -314,20 +332,22 @@ function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) { }) } -function executionError(promptID: PromptID, message: string) { - console.debug("[queueState] executionError", promptID, message) +function executionError(error: ComfyExecutionError): CompletedQueueEntry | null { + console.debug("[queueState] executionError", error) + let entry_ = null; store.update(s => { - const [index, entry, queue] = findEntryInPending(promptID); + const [index, entry, queue] = findEntryInPending(error.prompt_id); if (entry != null) { - moveToCompleted(index, queue, "error", "Error executing", message) + entry_ = moveToCompleted(index, queue, "error", "Error executing", error) } else { - console.error("[queueState] Could not find in pending! (executionError)", promptID) + console.error("[queueState] Could not find in pending! (executionError)", error.prompt_id) } s.progress = null; s.runningNodeID = null; return s }) + return entry_; } function createNewQueueEntry(promptID: PromptID, number: number = -1, prompt: SerializedPromptInputsAll = {}, extraData: any = {}): QueueEntry { @@ -432,6 +452,43 @@ function queueCleared(type: QueueItemType) { }) } +function promptError(workflowID: WorkflowInstID, error: ComfyAPIPromptErrorResponse, prompt: SerializedPrompt, extraData: ComfyBoxPromptExtraData): PromptID { + const workflowError: WorkflowValidationError = { + type: "validation", + workflowID, + error, + prompt, + extraData + } + + const entry: QueueEntry = { + number: 0, + queuedAt: new Date(), // Now + finishedAt: new Date(), + promptID: uuidv4(), // Just for keeping track + prompt: prompt.output, + extraData, + goodOutputs: [], + outputs: {}, + nodesRan: new Set(), + cachedNodes: new Set(), + } + + const completedEntry: CompletedQueueEntry = { + entry, + status: "validation_failed", + message: "Validation failed", + error: workflowError + } + + store.update(s => { + s.queueCompleted.update(qc => { qc.push(completedEntry); return qc }) + return s; + }) + + return entry.promptID; +} + const queueStateStore: WritableQueueStateStore = { ...store, @@ -447,6 +504,7 @@ const queueStateStore: WritableQueueStateStore = queueItemDeleted, queueCleared, getQueueEntry, - onExecuted + onExecuted, + promptError, } export default queueStateStore; diff --git a/src/lib/stores/uiState.ts b/src/lib/stores/uiState.ts index 39076ed..e28bc90 100644 --- a/src/lib/stores/uiState.ts +++ b/src/lib/stores/uiState.ts @@ -1,3 +1,4 @@ +import type { PromptID } from '$lib/api'; import { writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store'; @@ -11,7 +12,9 @@ export type UIState = { uiEditMode: UIEditMode, reconnecting: boolean, - forceSaveUserState: boolean | null + forceSaveUserState: boolean | null, + + activeError: PromptID | null } type UIStateOps = { @@ -30,6 +33,8 @@ const store: Writable = writable( reconnecting: false, forceSaveUserState: null, + + activeError: null }) function reconnecting() { diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts index d795c17..55ba772 100644 --- a/src/lib/stores/workflowState.ts +++ b/src/lib/stores/workflowState.ts @@ -8,8 +8,10 @@ import layoutStates from './layoutStates'; import { v4 as uuidv4 } from "uuid"; import type ComfyGraphCanvas from '$lib/ComfyGraphCanvas'; import { blankGraph } from '$lib/defaultGraph'; -import type { SerializedAppState } from '$lib/components/ComfyApp'; +import type { SerializedAppState, SerializedPrompt } from '$lib/components/ComfyApp'; import type ComfyReceiveOutputNode from '$lib/nodes/actions/ComfyReceiveOutputNode'; +import type { ComfyBoxPromptExtraData, PromptID } from '$lib/api'; +import type { ComfyAPIPromptErrorResponse, ComfyExecutionError } from '$lib/apiErrors'; type ActiveCanvas = { canvas: LGraphCanvas | null; @@ -63,6 +65,21 @@ export type WorkflowAttributes = { showDefaultNotifications: boolean, } +export type WorkflowValidationError = { + type: "validation" + workflowID: WorkflowInstID, + error: ComfyAPIPromptErrorResponse, + prompt: SerializedPrompt, + extraData: ComfyBoxPromptExtraData +} + +export type WorkflowExecutionError = { + type: "execution" + error: ComfyExecutionError, +} + +export type WorkflowError = WorkflowValidationError | WorkflowExecutionError; + export class ComfyBoxWorkflow { /* * Used for uniquely identifying the instance of the opened workflow in the frontend. @@ -89,6 +106,11 @@ export class ComfyBoxWorkflow { */ missingNodeTypes: Set = new Set(); + /* + * Completed queue entry ID that holds the last validation/execution error. + */ + lastError?: PromptID + get layout(): WritableLayoutStateStore | null { return layoutStates.getLayout(this.id) } @@ -257,7 +279,10 @@ type WorkflowStateOps = { closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void, closeAllWorkflows: (canvas: ComfyGraphCanvas) => void, setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number | WorkflowInstID) => ComfyBoxWorkflow | null, - findReceiveOutputTargets: (type: SlotType | SlotType[]) => WorkflowReceiveOutputTargets[] + findReceiveOutputTargets: (type: SlotType | SlotType[]) => WorkflowReceiveOutputTargets[], + afterQueued: (id: WorkflowInstID, promptID: PromptID) => void + promptError: (id: WorkflowInstID, promptID: PromptID) => void + executionError: (id: WorkflowInstID, promptID: PromptID) => void } export type WritableWorkflowStateStore = Writable & WorkflowStateOps; @@ -420,6 +445,36 @@ function findReceiveOutputTargets(type: SlotType | SlotType[]): WorkflowReceiveO return result; } +function afterQueued(id: WorkflowInstID, promptID: PromptID) { + const workflow = getWorkflow(id); + if (workflow == null) { + console.warn("[workflowState] afterQueued: workflow not found", id, promptID) + return + } + + workflow.lastError = null; +} + +function promptError(id: WorkflowInstID, promptID: PromptID) { + const workflow = getWorkflow(id); + if (workflow == null) { + console.warn("[workflowState] promptError: workflow not found", id, promptID) + return + } + + workflow.lastError = promptID; +} + +function executionError(id: WorkflowInstID, promptID: PromptID) { + const workflow = getWorkflow(id); + if (workflow == null) { + console.warn("[workflowState] executionError: workflow not found", id, promptID) + return + } + + workflow.lastError = promptID; +} + const workflowStateStore: WritableWorkflowStateStore = { ...store, @@ -434,6 +489,9 @@ const workflowStateStore: WritableWorkflowStateStore = closeWorkflow, closeAllWorkflows, setActiveWorkflow, - findReceiveOutputTargets + findReceiveOutputTargets, + afterQueued, + promptError, + executionError, } export default workflowStateStore; diff --git a/src/lib/widgets/ImageUploadWidget.svelte b/src/lib/widgets/ImageUploadWidget.svelte index 09f6fa6..a16f96b 100644 --- a/src/lib/widgets/ImageUploadWidget.svelte +++ b/src/lib/widgets/ImageUploadWidget.svelte @@ -6,7 +6,7 @@ import { get, writable, type Writable } from "svelte/store"; import Modal from "$lib/components/Modal.svelte"; import { Button } from "@gradio/button"; - import { Embed as Klecks } from "klecks"; + import { type Embed as Klecks } from "klecks"; import "klecks/style/style.scss"; import ImageUpload from "$lib/components/ImageUpload.svelte"; @@ -97,6 +97,8 @@ let blankImageWidth = 512; let blankImageHeight = 512; + let klecks: typeof import("klecks") | null = null; + async function openImageEditor() { if (!editorRoot) return; @@ -105,7 +107,9 @@ const url = configState.getBackendURL(); - kl = new Klecks({ + klecks ||= await import("klecks"); + + kl = new klecks.Embed({ embedUrl: url, onSubmit: submitKlecksToComfyUI, targetEl: editorRoot,