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 { createTemplate, serializeTemplate, type ComfyBoxTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
import type ComfyGraph from "./ComfyGraph";
@@ -24,11 +24,19 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
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,
@@ -97,6 +105,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
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;
@@ -126,6 +135,10 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
}
thickness = 2
}
else if (isHighlightedNode) {
color = "cyan";
thickness = 2
}
if (blink) {
if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) {
@@ -146,13 +159,28 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
}
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.strokeStyle = "red";
ctx.strokeStyle = color || "red";
for (const errorLocation of errors) {
if (errorLocation.input != null) {
if (errorLocation === this.blinkError && this.blinkErrorTime > 0) {
@@ -160,7 +188,17 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
continue;
}
}
const inputIndex = node.findInputSlotIndexByName(errorLocation.input.name)
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();
@@ -168,8 +206,6 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
ctx.stroke();
}
}
}
}
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;
@@ -656,22 +692,28 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
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);
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;
}
}

View File

@@ -5,6 +5,7 @@ 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",
@@ -165,6 +166,7 @@ export type ComfyGraphErrorLocation = {
errorType: ComfyNodeErrorType | "execution",
message: string,
dependentOutputs: NodeID[],
queueEntry: QueueEntry,
input?: ComfyGraphErrorInput,
@@ -182,7 +184,7 @@ export type ComfyGraphErrors = {
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[]> = {}
for (const [nodeID, nodeErrors] of Object.entries(validationError.node_errors)) {
@@ -194,6 +196,7 @@ export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validat
errorType: e.type,
message: e.message,
dependentOutputs: nodeErrors.dependent_outputs,
queueEntry
}
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[]> = {}
errorsByID[executionError.node_id] = [{
@@ -241,6 +244,7 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio
errorType: "execution",
message: executionError.message,
dependentOutputs: [], // TODO
queueEntry,
exceptionMessage: executionError.message,
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") {
return validationErrorToGraphErrors(workflowID, workflowError.error)
return validationErrorToGraphErrors(workflowID, workflowError.error, queueEntry)
}
else {
return executionErrorToGraphErrors(workflowID, workflowError.error)
return executionErrorToGraphErrors(workflowID, workflowError.error, queueEntry)
}
}

View File

@@ -246,7 +246,7 @@
app.resizeCanvas();
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();
}
@@ -271,8 +271,7 @@
function hideError() {
if (app?.lCanvas) {
app.lCanvas.activeErrors = null;
app.lCanvas.blinkError = null;
app.lCanvas.clearErrors();
}
}

View File

@@ -1,16 +1,17 @@
<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 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";
export let app: ComfyApp;
export let errors: ComfyGraphErrors;
function closeList() {
app.lCanvas.activeErrors = null;
app.lCanvas.blinkError = null;
app.lCanvas.clearErrors();
$uiState.activeError = null;
}
@@ -22,6 +23,34 @@
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) {
app.lCanvas.jumpToError(error);
}
@@ -66,6 +95,12 @@
<span>Type: {getInputTypeName(error.input.config[0])}</span>
{/if}
</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}
</div>
</div>

View File

@@ -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]
}
}

View File

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

View File

@@ -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;
}
</script>
<BlockLabel {show_label} Icon={Image} label={label || "Image"} />
{#if value === null}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{:else}
<div class="download">
<div class="buttons">
<a
href={value}
target={window.__is_colab__ ? "_blank" : null}
@@ -47,6 +51,7 @@
>
<IconButton Icon={Download} label="Download" />
</a>
<IconButton Icon={Clear} label="Remove" on:click={remove} />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img src={value} alt="" class:selectable on:click={handle_click} bind:naturalWidth={imageWidth} bind:naturalHeight={imageHeight} />
@@ -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);
}
</style>