Improve error jumping

This commit is contained in:
space-nuko
2023-05-27 01:13:06 -05:00
parent d144ec2ccd
commit a0b7418caf
7 changed files with 156 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
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 { 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 { get, type Unsubscriber } from "svelte/store";
import { createTemplate, serializeTemplate, type ComfyBoxTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate"; import { createTemplate, serializeTemplate, type ComfyBoxTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
import type ComfyGraph from "./ComfyGraph"; import type ComfyGraph from "./ComfyGraph";
@@ -24,11 +24,19 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
activeErrors?: ComfyGraphErrors = null; activeErrors?: ComfyGraphErrors = null;
blinkError: ComfyGraphErrorLocation | null = null; blinkError: ComfyGraphErrorLocation | null = null;
blinkErrorTime: number = 0; blinkErrorTime: number = 0;
highlightNodeAndInput: [LGraphNode, number] | null = null;
get comfyGraph(): ComfyGraph | null { get comfyGraph(): ComfyGraph | null {
return this.graph as ComfyGraph; return this.graph as ComfyGraph;
} }
clearErrors() {
this.activeErrors = null;
this.blinkError = null;
this.blinkErrorTime = 0;
this.highlightNodeAndInput = null;
}
constructor( constructor(
app: ComfyApp, app: ComfyApp,
canvas: HTMLCanvasElement | string, canvas: HTMLCanvasElement | string,
@@ -97,6 +105,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
const isRunningNode = node.id == state.runningNodeID const isRunningNode = node.id == state.runningNodeID
const nodeErrors = this.activeErrors?.errorsByID[node.id]; const nodeErrors = this.activeErrors?.errorsByID[node.id];
const isHighlightedNode = this.highlightNodeAndInput && this.highlightNodeAndInput[0].id === node.id;
if (this.blinkErrorTime > 0) { if (this.blinkErrorTime > 0) {
this.blinkErrorTime -= this.graph.elapsed_time; this.blinkErrorTime -= this.graph.elapsed_time;
@@ -126,6 +135,10 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
thickness = 2 thickness = 2
} }
else if (isHighlightedNode) {
color = "cyan";
thickness = 2
}
if (blink) { if (blink) {
if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) { if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) {
@@ -146,13 +159,28 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
} }
if (nodeErrors) { if (nodeErrors) {
this.drawFailedValidationInputs(node, nodeErrors, ctx); 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[], ctx: CanvasRenderingContext2D) { private drawFailedValidationInputs(node: LGraphNode, errors: ComfyGraphErrorLocation[], color: string, ctx: CanvasRenderingContext2D) {
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.strokeStyle = "red"; ctx.strokeStyle = color || "red";
for (const errorLocation of errors) { for (const errorLocation of errors) {
if (errorLocation.input != null) { if (errorLocation.input != null) {
if (errorLocation === this.blinkError && this.blinkErrorTime > 0) { if (errorLocation === this.blinkError && this.blinkErrorTime > 0) {
@@ -160,17 +188,25 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
continue; continue;
} }
} }
const inputIndex = node.findInputSlotIndexByName(errorLocation.input.name) this.highlightNodeInput(node, errorLocation.input.name, ctx);
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 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) { private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Vector2, mouseOver: boolean, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) {
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE; const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
@@ -656,22 +692,28 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
return return
} }
this.closeAllSubgraphs(); this.jumpToNode(node)
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);
this.highlightNodeAndInput = null;
this.blinkError = error; this.blinkError = error;
this.blinkErrorTime = 20; 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;
}
} }

View File

@@ -5,6 +5,7 @@ import type { SerializedPromptInputLink } from "./components/ComfyApp"
import type { WorkflowError, WorkflowInstID } from "./stores/workflowState" import type { WorkflowError, WorkflowInstID } from "./stores/workflowState"
import { exclude_internal_props } from "svelte/internal" import { exclude_internal_props } from "svelte/internal"
import type ComfyGraphCanvas from "./ComfyGraphCanvas" import type ComfyGraphCanvas from "./ComfyGraphCanvas"
import type { QueueEntry } from "./stores/queueState"
enum ComfyPromptErrorType { enum ComfyPromptErrorType {
NoOutputs = "prompt_no_outputs", NoOutputs = "prompt_no_outputs",
@@ -165,6 +166,7 @@ export type ComfyGraphErrorLocation = {
errorType: ComfyNodeErrorType | "execution", errorType: ComfyNodeErrorType | "execution",
message: string, message: string,
dependentOutputs: NodeID[], dependentOutputs: NodeID[],
queueEntry: QueueEntry,
input?: ComfyGraphErrorInput, input?: ComfyGraphErrorInput,
@@ -182,7 +184,7 @@ export type ComfyGraphErrors = {
errorsByID: Record<NodeID, ComfyGraphErrorLocation[]> errorsByID: Record<NodeID, ComfyGraphErrorLocation[]>
} }
export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validationError: ComfyAPIPromptErrorResponse): ComfyGraphErrors { export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validationError: ComfyAPIPromptErrorResponse, queueEntry: QueueEntry): ComfyGraphErrors {
const errorsByID: Record<NodeID, ComfyGraphErrorLocation[]> = {} const errorsByID: Record<NodeID, ComfyGraphErrorLocation[]> = {}
for (const [nodeID, nodeErrors] of Object.entries(validationError.node_errors)) { for (const [nodeID, nodeErrors] of Object.entries(validationError.node_errors)) {
@@ -194,6 +196,7 @@ export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validat
errorType: e.type, errorType: e.type,
message: e.message, message: e.message,
dependentOutputs: nodeErrors.dependent_outputs, dependentOutputs: nodeErrors.dependent_outputs,
queueEntry
} }
if (isInputWithValue(e.extra_info)) { if (isInputWithValue(e.extra_info)) {
@@ -231,7 +234,7 @@ export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validat
} }
} }
export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executionError: ComfyExecutionError): ComfyGraphErrors { export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executionError: ComfyExecutionError, queueEntry: QueueEntry): ComfyGraphErrors {
const errorsByID: Record<NodeID, ComfyGraphErrorLocation[]> = {} const errorsByID: Record<NodeID, ComfyGraphErrorLocation[]> = {}
errorsByID[executionError.node_id] = [{ errorsByID[executionError.node_id] = [{
@@ -241,6 +244,7 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio
errorType: "execution", errorType: "execution",
message: executionError.message, message: executionError.message,
dependentOutputs: [], // TODO dependentOutputs: [], // TODO
queueEntry,
exceptionMessage: executionError.message, exceptionMessage: executionError.message,
exceptionType: executionError.exception_type, exceptionType: executionError.exception_type,
@@ -256,11 +260,11 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio
} }
} }
export function workflowErrorToGraphErrors(workflowID: WorkflowInstID, workflowError: WorkflowError): ComfyGraphErrors { export function workflowErrorToGraphErrors(workflowID: WorkflowInstID, workflowError: WorkflowError, queueEntry: QueueEntry): ComfyGraphErrors {
if (workflowError.type === "validation") { if (workflowError.type === "validation") {
return validationErrorToGraphErrors(workflowID, workflowError.error) return validationErrorToGraphErrors(workflowID, workflowError.error, queueEntry)
} }
else { else {
return executionErrorToGraphErrors(workflowID, workflowError.error) return executionErrorToGraphErrors(workflowID, workflowError.error, queueEntry)
} }
} }

View File

@@ -246,7 +246,7 @@
app.resizeCanvas(); app.resizeCanvas();
app.lCanvas.draw(true, true); app.lCanvas.draw(true, true);
app.lCanvas.activeErrors = workflowErrorToGraphErrors(workflow.id, completed.error); app.lCanvas.activeErrors = workflowErrorToGraphErrors(workflow.id, completed.error, completed.entry);
app.lCanvas.jumpToFirstError(); app.lCanvas.jumpToFirstError();
} }
@@ -271,8 +271,7 @@
function hideError() { function hideError() {
if (app?.lCanvas) { if (app?.lCanvas) {
app.lCanvas.activeErrors = null; app.lCanvas.clearErrors();
app.lCanvas.blinkError = null;
} }
} }

View File

@@ -1,16 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { ComfyGraphErrorLocation, ComfyGraphErrors } from "$lib/apiErrors"; import { ComfyNodeErrorType, type ComfyGraphErrorLocation, type ComfyGraphErrors } from "$lib/apiErrors";
import type ComfyApp from "./ComfyApp"; import type ComfyApp from "./ComfyApp";
import Accordion from "./gradio/app/Accordion.svelte"; import Accordion from "./gradio/app/Accordion.svelte";
import uiState from '$lib/stores/uiState'; import uiState from '$lib/stores/uiState';
import type { ComfyNodeDefInputType } from "$lib/ComfyNodeDef"; import type { ComfyNodeDefInputType } from "$lib/ComfyNodeDef";
import type { INodeInputSlot, LGraphNode, Subgraph } from "@litegraph-ts/core";
import { UpstreamNodeLocator } from "./ComfyPromptSerializer";
export let app: ComfyApp; export let app: ComfyApp;
export let errors: ComfyGraphErrors; export let errors: ComfyGraphErrors;
function closeList() { function closeList() {
app.lCanvas.activeErrors = null; app.lCanvas.clearErrors();
app.lCanvas.blinkError = null;
$uiState.activeError = null; $uiState.activeError = null;
} }
@@ -22,6 +23,34 @@
return node.graph._subgraph_node return node.graph._subgraph_node
} }
function canJumpToDisconnectedInput(error: ComfyGraphErrorLocation): boolean {
return error.errorType === ComfyNodeErrorType.RequiredInputMissing && error.input != null;
}
function jumpToDisconnectedInput(error: ComfyGraphErrorLocation) {
if (error.errorType !== ComfyNodeErrorType.RequiredInputMissing || error.input == null) {
return
}
const node = app.lCanvas.graph.getNodeByIdRecursive(error.nodeID);
const inputIndex =node.findInputSlotIndexByName(error.input.name);
if (inputIndex === -1) {
return
}
// TODO multiple tags?
const tag: string | null = error.queueEntry.extraData.extra_pnginfo.comfyBoxPrompt.subgraphs[0];
const test = (node: LGraphNode) => (node as any).isBackendNode
const nodeLocator = new UpstreamNodeLocator(test)
const [_, foundLink, foundInputSlot, foundPrevNode] = nodeLocator.locateUpstream(node, inputIndex, tag);
if (foundInputSlot != null && foundPrevNode != null) {
app.lCanvas.jumpToNodeAndInput(foundPrevNode, foundInputSlot);
}
}
function jumpToError(error: ComfyGraphErrorLocation) { function jumpToError(error: ComfyGraphErrorLocation) {
app.lCanvas.jumpToError(error); app.lCanvas.jumpToError(error);
} }
@@ -66,6 +95,12 @@
<span>Type: {getInputTypeName(error.input.config[0])}</span> <span>Type: {getInputTypeName(error.input.config[0])}</span>
{/if} {/if}
</div> </div>
{#if canJumpToDisconnectedInput(error)}
<div style:display="flex" style:flex-direction="row">
<button class="jump-to-error" on:click={() => jumpToDisconnectedInput(error)}><span></span></button>
<span>Find disconnected input</span>
</div>
{/if}
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -71,11 +71,13 @@ export function isActiveBackendNode(node: LGraphNode, tag: string | null = null)
return true; return true;
} }
type UpstreamResult = [LGraph | null, LLink | null, number | null, LGraphNode | null];
export class UpstreamNodeLocator { export class UpstreamNodeLocator {
constructor(private isTheTargetNode: (node: LGraphNode) => boolean) { 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) if (link.origin_id != subgraph.id)
throw new Error("Invalid link and graph output!") throw new Error("Invalid link and graph output!")
@@ -84,10 +86,10 @@ export class UpstreamNodeLocator {
throw new Error("No inner graph input!") throw new Error("No inner graph input!")
const nextLink = innerGraphOutput.getInputLink(0) 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) if (link.origin_id != graphInput.id)
throw new Error("Invalid link and graph input!") throw new Error("Invalid link and graph input!")
@@ -100,10 +102,10 @@ export class UpstreamNodeLocator {
throw new Error("No outer input slot!") throw new Error("No outer input slot!")
const nextLink = outerSubgraph.getInputLink(outerInputIndex) 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)) { if (parent.is(Subgraph)) {
console.debug("FollowSubgraph") console.debug("FollowSubgraph")
return this.followSubgraph(parent, currentLink); return this.followSubgraph(parent, currentLink);
@@ -113,17 +115,18 @@ export class UpstreamNodeLocator {
return this.followGraphInput(parent, currentLink); return this.followGraphInput(parent, currentLink);
} }
else if ("getUpstreamLink" in parent) { 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) { else if (parent.inputs.length === 1) {
// Only one input, so assume we can follow it backwards. // Only one input, so assume we can follow it backwards.
const link = parent.getInputLink(0); const link = parent.getInputLink(0);
if (link) { if (link) {
return [parent.graph, link] return [parent.graph, link, 0, parent]
} }
} }
console.warn("[graphToPrompt] Frontend node does not support getUpstreamLink", parent.type) 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 * Returns the node and the output link attached to it that leads to the
* starting node if any. * 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); let parent = fromNode.getInputNode(inputIndex);
if (!parent) if (!parent)
return [null, null]; return [null, null, null, null];
const seen = {} const seen = {}
let currentLink = fromNode.getInputLink(inputIndex); let currentLink = fromNode.getInputLink(inputIndex);
let currentInputSlot = inputIndex;
let currentNode = fromNode;
const shouldFollowParent = (parent: LGraphNode) => { const shouldFollowParent = (parent: LGraphNode) => {
return isActiveNode(parent, tag) && !this.isTheTargetNode(parent); return isActiveNode(parent, tag) && !this.isTheTargetNode(parent);
@@ -152,7 +157,10 @@ export class UpstreamNodeLocator {
// nodes have conditional logic that determines which link // nodes have conditional logic that determines which link
// to follow backwards. // to follow backwards.
while (shouldFollowParent(parent)) { 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) { if (nextLink == null) {
console.warn("[graphToPrompt] No upstream link found in frontend node", parent) 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) 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]
} }
} }

View File

@@ -148,8 +148,11 @@
&.success { &.success {
/* background: green; */ /* background: green; */
} }
&.error, &.validation_failed { &.validation_failed {
background: red; background: #551a1a;
}
&.error {
background: #401a40;
} }
&.all_cached, &.interrupted { &.all_cached, &.interrupted {
filter: brightness(80%); filter: brightness(80%);

View File

@@ -2,7 +2,7 @@
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { SelectData } from "@gradio/utils"; import type { SelectData } from "@gradio/utils";
import { BlockLabel, Empty, IconButton } from "@gradio/atoms"; 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 { get_coordinates_of_clicked_image } from "./utils";
import { Image } from "@gradio/icons"; import { Image } from "@gradio/icons";
@@ -33,13 +33,17 @@
dispatch("select", { index: coordinates, value: null }); dispatch("select", { index: coordinates, value: null });
} }
}; };
function remove() {
value = null;
}
</script> </script>
<BlockLabel {show_label} Icon={Image} label={label || "Image"} /> <BlockLabel {show_label} Icon={Image} label={label || "Image"} />
{#if value === null} {#if value === null}
<Empty size="large" unpadded_box={true}><Image /></Empty> <Empty size="large" unpadded_box={true}><Image /></Empty>
{:else} {:else}
<div class="download"> <div class="buttons">
<a <a
href={value} href={value}
target={window.__is_colab__ ? "_blank" : null} target={window.__is_colab__ ? "_blank" : null}
@@ -47,6 +51,7 @@
> >
<IconButton Icon={Download} label="Download" /> <IconButton Icon={Download} label="Download" />
</a> </a>
<IconButton Icon={Clear} label="Remove" on:click={remove} />
</div> </div>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<img src={value} alt="" class:selectable on:click={handle_click} bind:naturalWidth={imageWidth} bind:naturalHeight={imageHeight} /> <img src={value} alt="" class:selectable on:click={handle_click} bind:naturalWidth={imageWidth} bind:naturalHeight={imageHeight} />
@@ -63,9 +68,13 @@
cursor: crosshair; cursor: crosshair;
} }
.download { .buttons {
display: flex;
position: absolute; position: absolute;
top: 6px; top: var(--size-2);
right: 6px; right: var(--size-2);
justify-content: flex-end;
gap: var(--spacing-sm);
z-index: var(--layer-5);
} }
</style> </style>