Support as-yet-released error API
upp
This commit is contained in:
@@ -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<string, SerializedPromptInput>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<script context="module" lang="ts">
|
||||
export const WORKFLOWS_VIEW: any = {}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { PlusSquareDotted } from 'svelte-bootstrap-icons';
|
||||
@@ -12,12 +16,15 @@
|
||||
import workflowState, { ComfyBoxWorkflow, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import type ComfyApp from './ComfyApp';
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, setContext, tick } from "svelte";
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { cubicIn } from 'svelte/easing';
|
||||
import { truncateString } from '$lib/utils';
|
||||
import ComfyPaneView from './ComfyPaneView.svelte';
|
||||
import type { PromptID } from '$lib/api';
|
||||
import queueState, { type CompletedQueueEntry } from '$lib/stores/queueState';
|
||||
import { workflowErrorToGraphErrors } from '$lib/apiErrors';
|
||||
|
||||
export let app: ComfyApp;
|
||||
export let uiTheme: string = "gradio-dark" // TODO config
|
||||
@@ -44,6 +51,8 @@
|
||||
}
|
||||
);
|
||||
refreshView();
|
||||
|
||||
lastError = null;
|
||||
})
|
||||
|
||||
async function doRefreshCombos() {
|
||||
@@ -70,18 +79,22 @@
|
||||
|
||||
let graphSize = 0;
|
||||
|
||||
$: if (containerElem) {
|
||||
function getGraphPane(): HTMLDivElement | null {
|
||||
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
|
||||
if (canvas) {
|
||||
const paneNode = canvas.closest(".splitpanes__pane")
|
||||
if (paneNode) {
|
||||
(paneNode as HTMLElement).ontransitionstart = () => {
|
||||
$interfaceState.graphTransitioning = true
|
||||
}
|
||||
(paneNode as HTMLElement).ontransitionend = () => {
|
||||
$interfaceState.graphTransitioning = false
|
||||
app.resizeCanvas()
|
||||
}
|
||||
if (!canvas)
|
||||
return null;
|
||||
return canvas.closest(".splitpanes__pane")
|
||||
}
|
||||
|
||||
$: if (containerElem) {
|
||||
const paneNode = getGraphPane();
|
||||
if (paneNode) {
|
||||
(paneNode as HTMLElement).ontransitionstart = () => {
|
||||
$interfaceState.graphTransitioning = true
|
||||
}
|
||||
(paneNode as HTMLElement).ontransitionend = () => {
|
||||
$interfaceState.graphTransitioning = false
|
||||
app.resizeCanvas()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +204,79 @@
|
||||
return s;
|
||||
})
|
||||
};
|
||||
|
||||
let lastError: PromptID | null = null;
|
||||
|
||||
$: {
|
||||
const activeError = $uiState.activeError
|
||||
if (activeError != lastError) {
|
||||
if (activeError != null) {
|
||||
showError(activeError)
|
||||
}
|
||||
else {
|
||||
hideError();
|
||||
}
|
||||
lastError = $uiState.activeError;
|
||||
}
|
||||
else if (activeError == null) {
|
||||
hideError();
|
||||
lastError = null
|
||||
}
|
||||
}
|
||||
|
||||
async function showError(promptIDWithError: PromptID) {
|
||||
hideError();
|
||||
|
||||
const completed: CompletedQueueEntry = get($queueState.queueCompleted).find(e => e.entry.promptID === promptIDWithError);
|
||||
if (!completed || !completed.error) {
|
||||
console.error("Prompt with error not found!", promptIDWithError);
|
||||
return
|
||||
}
|
||||
|
||||
const workflow = workflowState.getWorkflow(completed.entry.extraData.workflowID)
|
||||
if (workflow == null) {
|
||||
console.error("Workflow with error not found!", promptIDWithError, completed);
|
||||
return
|
||||
}
|
||||
|
||||
workflowState.setActiveWorkflow(app.lCanvas, workflow.id);
|
||||
|
||||
const jumpToError = () => {
|
||||
app.resizeCanvas();
|
||||
app.lCanvas.draw(true, true);
|
||||
|
||||
app.lCanvas.activeErrors = workflowErrorToGraphErrors(workflow.id, completed.error);
|
||||
app.lCanvas.jumpToFirstError();
|
||||
}
|
||||
|
||||
const newGraphSize = Math.max(50, graphSize);
|
||||
const willOpenPane = newGraphSize != graphSize
|
||||
graphSize = newGraphSize
|
||||
|
||||
if (willOpenPane) {
|
||||
const graphPane = getGraphPane();
|
||||
if (graphPane) {
|
||||
graphPane.addEventListener("transitionend", jumpToError)
|
||||
await tick()
|
||||
}
|
||||
else {
|
||||
jumpToError()
|
||||
}
|
||||
}
|
||||
else {
|
||||
jumpToError()
|
||||
}
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
if (app?.lCanvas) {
|
||||
app.lCanvas.activeErrors = null;
|
||||
}
|
||||
}
|
||||
|
||||
setContext(WORKFLOWS_VIEW, {
|
||||
showError
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="comfy-content" bind:this={containerElem} class:loading>
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "@gradio/button";
|
||||
import { onMount } from "svelte";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
import DropZone from "./DropZone.svelte";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
import uiState from '$lib/stores/uiState';
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount(async () => {
|
||||
if (app?.lCanvas && canvas) {
|
||||
app.lCanvas?.setCanvas(canvas)
|
||||
}
|
||||
})
|
||||
|
||||
function doRecenter(): void {
|
||||
app?.lCanvas?.recenter();
|
||||
}
|
||||
|
||||
function clearErrors(): void {
|
||||
$uiState.activeError = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper litegraph">
|
||||
<div class="canvas-wrapper pane-wrapper">
|
||||
<canvas id="graph-canvas" />
|
||||
<canvas bind:this={canvas} id="graph-canvas" />
|
||||
<DropZone {app} />
|
||||
</div>
|
||||
<div class="bar">
|
||||
{#if !$interfaceState.graphTransitioning}
|
||||
<span class="left">
|
||||
<button on:click={doRecenter}>Recenter</button>
|
||||
{#if $uiState.activeError != null}
|
||||
<button on:click={clearErrors}>Clear Errors</button>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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<CompletedQueueEntry[]> | 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() {
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
&.success {
|
||||
/* background: green; */
|
||||
}
|
||||
&.error {
|
||||
&.error, &.validation_failed {
|
||||
background: red;
|
||||
}
|
||||
&.all_cached, &.interrupted {
|
||||
|
||||
18
src/lib/components/OnClickToastItem.svelte
Normal file
18
src/lib/components/OnClickToastItem.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { NotifyOptions } from "$lib/notify";
|
||||
import { toast } from "@zerodevx/svelte-toast";
|
||||
|
||||
export let message: string = ""
|
||||
export let notifyOptions: NotifyOptions
|
||||
export let toastID: string
|
||||
|
||||
function onClick() {
|
||||
if (notifyOptions.onClick) {
|
||||
notifyOptions.onClick()
|
||||
toast.pop(toastID)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div on:click={onClick}>{message}</div>
|
||||
Reference in New Issue
Block a user