From 51e02f817988affd75e03df5b1dec1ef1a7d2d67 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 26 May 2023 19:50:07 -0500 Subject: [PATCH 01/11] Fix subgraphs queue string --- src/lib/components/ComfyQueue.svelte | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index 3b66d04..907ce86 100644 --- a/src/lib/components/ComfyQueue.svelte +++ b/src/lib/components/ComfyQueue.svelte @@ -144,9 +144,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}` From 1da8dc35ec2f9c61015fd4bd060d3b8fe675df63 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 26 May 2023 19:50:19 -0500 Subject: [PATCH 02/11] Reuse sent gallery image width/height --- src/lib/nodes/widgets/ComfyGalleryNode.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 Date: Fri, 26 May 2023 23:04:25 -0500 Subject: [PATCH 03/11] Support as-yet-released error API upp --- src/lib/ComfyGraphCanvas.ts | 78 ++++- src/lib/api.ts | 20 +- src/lib/apiErrors.ts | 266 ++++++++++++++++++ src/lib/components/ComfyApp.ts | 65 ++++- .../components/ComfyBoxWorkflowsView.svelte | 110 +++++++- src/lib/components/ComfyGraphView.svelte | 21 +- src/lib/components/ComfyQueue.svelte | 32 ++- .../components/ComfyQueueListDisplay.svelte | 2 +- src/lib/components/OnClickToastItem.svelte | 18 ++ src/lib/notify.ts | 62 ++-- src/lib/stores/queueState.ts | 92 ++++-- src/lib/stores/uiState.ts | 7 +- src/lib/stores/workflowState.ts | 64 ++++- src/lib/widgets/ImageUploadWidget.svelte | 8 +- 14 files changed, 758 insertions(+), 87 deletions(-) create mode 100644 src/lib/apiErrors.ts create mode 100644 src/lib/components/OnClickToastItem.svelte diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index c3b792e..d66f47d 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 } 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,6 +21,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { app: ComfyApp | null; private _unsubscribe: Unsubscriber; isExportingSVG: boolean = false; + activeErrors?: ComfyGraphErrors = null; get comfyGraph(): ComfyGraph | null { return this.graph as ComfyGraph; @@ -90,7 +92,9 @@ 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]; let color = null; let thickness = 1; @@ -104,6 +108,16 @@ export default class ComfyGraphCanvas extends LGraphCanvas { else if (isRunningNode) { color = "#0f0"; } + else if (nodeErrors) { + const hasExecutionError = nodeErrors.find(e => e.errorType === "execution"); + if (hasExecutionError) { + color = "#f0f"; + } + else { + color = "red"; + } + thickness = 2 + } if (color) { this.drawNodeOutline(node, ctx, size, mouseOver, fgColor, bgColor, color, thickness) @@ -114,6 +128,26 @@ 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, ctx); + } + } + + private drawFailedValidationInputs(node: LGraphNode, errors: ComfyGraphErrorLocation[], ctx: CanvasRenderingContext2D) { + ctx.lineWidth = 2; + ctx.strokeStyle = "red"; + for (const errorLocation of errors) { + if (errorLocation.input != null) { + const inputIndex = node.findInputSlotIndexByName(errorLocation.input.name) + 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 +602,40 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } return false; } + + jumpToFirstError() { + this.jumpToError(0); + } + + jumpToError(index: number) { + if (this.activeErrors == null) { + return; + } + + const error = this.activeErrors.errors[index] + if (error == null) { + return; + } + + const node = this.graph.getNodeByIdRecursive(error.nodeID); + if (node == null) { + notify(`Couldn't find node '${error.comfyNodeType}' (${error.nodeID})`, { type: "warning" }) + return + } + + this.closeAllSubgraphs(); + + const subgraphs: LGraph[] = [] + let node_ = node; + while (node_.graph._is_subgraph) { + subgraphs.push(node.graph); + node_ = node.graph._subgraph_node + } + + for (const subgraph of subgraphs) { + this.openSubgraph(subgraph) + } + + this.centerOnNode(node); + } } 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..5a26ee0 --- /dev/null +++ b/src/lib/apiErrors.ts @@ -0,0 +1,266 @@ +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" + +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 & { + 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.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[], + + 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): 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, + } + + 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): ComfyGraphErrors { + const errorsByID: Record = {} + + errorsByID[executionError.node_id] = [{ + workflowID, + nodeID: executionError.node_id, + comfyNodeType: executionError.node_type, + errorType: "execution", + message: executionError.message, + dependentOutputs: [], // TODO + + exceptionMessage: executionError.message, + exceptionType: executionError.exception_type, + traceback: executionError.traceback, + inputValues: executionError.current_inputs, + outputValues: executionError.current_outputs, + }] + + return { + message: executionError.message, + errors: Object.values(errorsByID).flatMap(e => e), + errorsByID + } +} + +export function workflowErrorToGraphErrors(workflowID: WorkflowInstID, workflowError: WorkflowError): ComfyGraphErrors { + if (workflowError.type === "validation") { + return validationErrorToGraphErrors(workflowID, workflowError.error) + } + else { + return executionErrorToGraphErrors(workflowID, workflowError.error) + } +} diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 551abac..d15cc2b 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; @@ -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..d3ed091 100644 --- a/src/lib/components/ComfyBoxWorkflowsView.svelte +++ b/src/lib/components/ComfyBoxWorkflowsView.svelte @@ -1,3 +1,7 @@ + +
diff --git a/src/lib/components/ComfyGraphView.svelte b/src/lib/components/ComfyGraphView.svelte index 44b35ac..f26ec8d 100644 --- a/src/lib/components/ComfyGraphView.svelte +++ b/src/lib/components/ComfyGraphView.svelte @@ -1,25 +1,42 @@
- +
{#if !$interfaceState.graphTransitioning} + {#if $uiState.activeError != null} + + {/if} {/if}
diff --git a/src/lib/components/ComfyQueue.svelte b/src/lib/components/ComfyQueue.svelte index 907ce86..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 @@ -200,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; } @@ -234,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..1a28da8 100644 --- a/src/lib/components/ComfyQueueListDisplay.svelte +++ b/src/lib/components/ComfyQueueListDisplay.svelte @@ -148,7 +148,7 @@ &.success { /* background: green; */ } - &.error { + &.error, &.validation_failed { background: red; } &.all_cached, &.interrupted { 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/notify.ts b/src/lib/notify.ts index 167980d..6f6a3ef 100644 --- a/src/lib/notify.ts +++ b/src/lib/notify.ts @@ -2,13 +2,16 @@ import { toast } from "@zerodevx/svelte-toast"; import type { SvelteToastOptions } from "@zerodevx/svelte-toast/stores"; import { type Notification } from "framework7/components/notification" import { f7 } from "framework7-svelte" +import OnClickToastItem from "$lib/components/OnClickToastItem.svelte" export type NotifyOptions = { title?: string, type?: "neutral" | "info" | "warning" | "error" | "success", imageUrl?: string, timeout?: number | null, - showOn?: "web" | "native" | "all" | "none" + showOn?: "web" | "native" | "all" | "none", + showBar?: boolean, + onClick?: () => 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, + 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, From d144ec2ccde6e15e555f6b539a0d6d8c44c7179d Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 00:21:55 -0500 Subject: [PATCH 04/11] Show error list --- src/lib/ComfyGraphCanvas.ts | 42 +++- src/lib/components/ComfyApp.ts | 2 +- .../components/ComfyBoxWorkflowsView.svelte | 2 + src/lib/components/ComfyGraphErrorList.svelte | 228 ++++++++++++++++++ src/lib/components/ComfyGraphView.svelte | 7 +- src/lib/notify.ts | 2 +- 6 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 src/lib/components/ComfyGraphErrorList.svelte diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index d66f47d..b63f9a5 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -22,6 +22,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas { private _unsubscribe: Unsubscriber; isExportingSVG: boolean = false; activeErrors?: ComfyGraphErrors = null; + blinkError: ComfyGraphErrorLocation | null = null; + blinkErrorTime: number = 0; get comfyGraph(): ComfyGraph | null { return this.graph as ComfyGraph; @@ -96,8 +98,13 @@ export default class ComfyGraphCanvas extends LGraphCanvas { const isRunningNode = node.id == state.runningNodeID const nodeErrors = this.activeErrors?.errorsByID[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; @@ -111,6 +118,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { else if (nodeErrors) { const hasExecutionError = nodeErrors.find(e => e.errorType === "execution"); if (hasExecutionError) { + blink = true; color = "#f0f"; } else { @@ -119,6 +127,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { 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) } @@ -139,6 +155,11 @@ export default class ComfyGraphCanvas extends LGraphCanvas { ctx.strokeStyle = "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; + } + } const inputIndex = node.findInputSlotIndexByName(errorLocation.input.name) if (inputIndex !== -1) { let pos = node.getConnectionPos(true, inputIndex); @@ -607,17 +628,29 @@ export default class ComfyGraphCanvas extends LGraphCanvas { this.jumpToError(0); } - jumpToError(index: number) { + jumpToError(index: number | ComfyGraphErrorLocation) { if (this.activeErrors == null) { return; } - const error = this.activeErrors.errors[index] + let error; + if (typeof index === "number") { + error = this.activeErrors.errors[index] + } + else { + error = index; + } + if (error == null) { return; } - const node = this.graph.getNodeByIdRecursive(error.nodeID); + 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 @@ -637,5 +670,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } this.centerOnNode(node); + + this.blinkError = error; + this.blinkErrorTime = 20; } } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index d15cc2b..9f905be 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -309,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] }) diff --git a/src/lib/components/ComfyBoxWorkflowsView.svelte b/src/lib/components/ComfyBoxWorkflowsView.svelte index d3ed091..49b798e 100644 --- a/src/lib/components/ComfyBoxWorkflowsView.svelte +++ b/src/lib/components/ComfyBoxWorkflowsView.svelte @@ -240,6 +240,7 @@ } workflowState.setActiveWorkflow(app.lCanvas, workflow.id); + $uiState.activeError = promptIDWithError; const jumpToError = () => { app.resizeCanvas(); @@ -271,6 +272,7 @@ function hideError() { if (app?.lCanvas) { app.lCanvas.activeErrors = null; + app.lCanvas.blinkError = null; } } diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte new file mode 100644 index 0000000..e435c18 --- /dev/null +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -0,0 +1,228 @@ + + +
+
+ +
+ {#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}) + {:else if error.input} +
+ Input Name: {error.input.name} + {#if error.input.config} + Type: {getInputTypeName(error.input.config[0])} + {/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 f26ec8d..081f0fa 100644 --- a/src/lib/components/ComfyGraphView.svelte +++ b/src/lib/components/ComfyGraphView.svelte @@ -5,13 +5,15 @@ import interfaceState from "$lib/stores/interfaceState"; import workflowState from "$lib/stores/workflowState"; import uiState from '$lib/stores/uiState'; + import ComfyGraphErrorList from "$lib/components/ComfyGraphErrorList.svelte" export let app: ComfyApp; let canvas: HTMLCanvasElement; onMount(async () => { - if (app?.lCanvas && canvas) { + if (app?.lCanvas) { + canvas = app.lCanvas.canvas; app.lCanvas?.setCanvas(canvas) } }) @@ -40,6 +42,9 @@ {/if}
+ {#if $uiState.activeError && app?.lCanvas?.activeErrors != null} + + {/if}
From 49ede4e2c8ee62d44d8a22f957ddfefa46f51187 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 01:30:20 -0500 Subject: [PATCH 06/11] Fix --- src/lib/components/ComfyBoxWorkflowsView.svelte | 7 ++----- src/lib/components/ComfyGraphErrorList.svelte | 5 ++++- src/lib/components/ComfyGraphView.svelte | 17 +++++++++-------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/lib/components/ComfyBoxWorkflowsView.svelte b/src/lib/components/ComfyBoxWorkflowsView.svelte index 865ecb2..816dc39 100644 --- a/src/lib/components/ComfyBoxWorkflowsView.svelte +++ b/src/lib/components/ComfyBoxWorkflowsView.svelte @@ -218,10 +218,6 @@ } lastError = $uiState.activeError; } - else if (activeError == null) { - hideError(); - lastError = null - } } async function showError(promptIDWithError: PromptID) { @@ -241,6 +237,7 @@ workflowState.setActiveWorkflow(app.lCanvas, workflow.id); $uiState.activeError = promptIDWithError; + lastError = $uiState.activeError; const jumpToError = () => { app.resizeCanvas(); @@ -257,7 +254,7 @@ if (willOpenPane) { const graphPane = getGraphPane(); if (graphPane) { - graphPane.addEventListener("transitionend", jumpToError) + graphPane.addEventListener("transitionend", jumpToError, { once: true }) await tick() } else { diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte index a676874..694874d 100644 --- a/src/lib/components/ComfyGraphErrorList.svelte +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -97,7 +97,7 @@ {#if canJumpToDisconnectedInput(error)}
- + Find disconnected input
{/if} @@ -216,6 +216,9 @@ &.execution-error { background: #848; } + &.locate { + background: #488; + } width: 32px; height: 32px; font-size: 14pt; diff --git a/src/lib/components/ComfyGraphView.svelte b/src/lib/components/ComfyGraphView.svelte index 081f0fa..6dea59b 100644 --- a/src/lib/components/ComfyGraphView.svelte +++ b/src/lib/components/ComfyGraphView.svelte @@ -33,14 +33,12 @@
- {#if !$interfaceState.graphTransitioning} - - - {#if $uiState.activeError != null} - - {/if} - - {/if} + + + {#if $uiState.activeError != null} + + {/if} +
{#if $uiState.activeError && app?.lCanvas?.activeErrors != null} @@ -95,6 +93,9 @@ background-color: #555; border-color: #777; } + &:disabled { + opacity: 50%; + } } } From 35b991728f92ab2dec30ca0498e7db457be012bb Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 01:53:49 -0500 Subject: [PATCH 07/11] Fix --- src/lib/components/ComfyGraphErrorList.svelte | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte index 694874d..99dcad9 100644 --- a/src/lib/components/ComfyGraphErrorList.svelte +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -88,11 +88,15 @@ {error.message} {#if error.exceptionType} ({error.exceptionType}) - {:else if error.input} + {/if} + {#if error.exceptionMessage} +
{error.exceptionMessage}
+ {/if} + {#if error.input}
- Input Name: {error.input.name} + Input: `{error.input.name}` {#if error.input.config} - Type: {getInputTypeName(error.input.config[0])} + ({getInputTypeName(error.input.config[0])}) {/if}
{#if canJumpToDisconnectedInput(error)} From 17d6e68b757b9b0b7938eb971a7d42eae4b84470 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 02:02:32 -0500 Subject: [PATCH 08/11] Update --- src/lib/apiErrors.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/apiErrors.ts b/src/lib/apiErrors.ts index f027f94..e631cbc 100644 --- a/src/lib/apiErrors.ts +++ b/src/lib/apiErrors.ts @@ -135,7 +135,7 @@ export type ComfyInterruptedError = { } export type ComfyExecutionError = ComfyInterruptedError & { - message: string, + exception_message: string, exception_type: string, traceback: string[], current_inputs: any[], @@ -147,7 +147,7 @@ export function formatValidationError(error: ComfyAPIPromptErrorResponse) { } export function formatExecutionError(error: ComfyExecutionError) { - return `${error.message}` + return error.exception_message } export type ComfyGraphErrorInput = { @@ -242,11 +242,11 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio nodeID: executionError.node_id, comfyNodeType: executionError.node_type, errorType: "execution", - message: executionError.message, + message: executionError.exception_message, dependentOutputs: [], // TODO queueEntry, - exceptionMessage: executionError.message, + exceptionMessage: executionError.exception_message, exceptionType: executionError.exception_type, traceback: executionError.traceback, inputValues: executionError.current_inputs, @@ -254,7 +254,7 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio }] return { - message: executionError.message, + message: executionError.exception_message, errors: Object.values(errorsByID).flatMap(e => e), errorsByID } From 5cb93e658121f0971a37ef7826d119dd122f8b06 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 02:04:20 -0500 Subject: [PATCH 09/11] Fix --- src/lib/components/ComfyGraphErrorList.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte index 99dcad9..6cdeb9b 100644 --- a/src/lib/components/ComfyGraphErrorList.svelte +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -89,7 +89,7 @@ {#if error.exceptionType} ({error.exceptionType}) {/if} - {#if error.exceptionMessage} + {#if error.exceptionMessage && !isExecutionError}
{error.exceptionMessage}
{/if} {#if error.input} From a3eddfc3503530b9ff3f3437cf4c3f3aa6bb4f1d Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 02:07:25 -0500 Subject: [PATCH 10/11] Use pre --- src/lib/components/ComfyGraphErrorList.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte index 6cdeb9b..fdb6cc8 100644 --- a/src/lib/components/ComfyGraphErrorList.svelte +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -115,7 +115,7 @@
{#each error.traceback as line} -
{line}
+
{line}
{/each}
From 88d99d2bcb19a95856091b69f50f21024cd7f629 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 27 May 2023 09:35:19 -0500 Subject: [PATCH 11/11] Improve --- src/lib/components/ComfyGraphErrorList.svelte | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte index fdb6cc8..ad68d26 100644 --- a/src/lib/components/ComfyGraphErrorList.svelte +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -4,8 +4,9 @@ import Accordion from "./gradio/app/Accordion.svelte"; import uiState from '$lib/stores/uiState'; import type { ComfyNodeDefInputType } from "$lib/ComfyNodeDef"; - import type { INodeInputSlot, LGraphNode, Subgraph } from "@litegraph-ts/core"; - import { UpstreamNodeLocator } from "./ComfyPromptSerializer"; + import type { INodeInputSlot, LGraphNode, Subgraph } from "@litegraph-ts/core"; + import { UpstreamNodeLocator } from "./ComfyPromptSerializer"; + import JsonView from "./JsonView.svelte"; export let app: ComfyApp; export let errors: ComfyGraphErrors; @@ -84,7 +85,7 @@
-
+
{error.message} {#if error.exceptionType} ({error.exceptionType}) @@ -94,7 +95,7 @@ {/if} {#if error.input}
- Input: `{error.input.name}` + Input: {error.input.name} {#if error.input.config} ({getInputTypeName(error.input.config[0])}) {/if} @@ -105,6 +106,28 @@ 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}
@@ -188,6 +211,7 @@ } .error-details { + width: 100%; display: flex; flex-direction: row; gap: var(--spacing-md); @@ -202,6 +226,10 @@ } } + .error-details-wrapper { + flex: 5 1 0%; + } + .error-message { color: #F66; &.execution-error { @@ -246,19 +274,20 @@ } .error-traceback-wrapper { + width: 100%; margin-top: 1.0rem; padding: 0.5rem; border: 1px solid #888; .error-traceback { - font-size: 11pt; + font-size: 10pt; overflow: auto; white-space: nowrap; background: #333; .error-traceback-contents { width: 100%; - font-family: monospace; + font-family: monospace !important; padding: 1.0rem; > div {