diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 8da6407..331e26c 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -4,7 +4,10 @@ import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, ty import { importA1111, parsePNGMetadata } from "$lib/pnginfo"; import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; -import A1111PromptDisplay from "./A1111PromptDisplay.svelte"; +import A1111PromptModal from "./modal/A1111PromptModal.svelte"; +import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte"; +import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte"; +import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte"; // Import nodes @@ -149,6 +152,11 @@ export type WorkflowLoadError = { error: Error } +export type VanillaWorkflowConvertResult = { + comfyBoxWorkflow: SerializedAppState, + missingNodeTypes: Set +} + function isComfyBoxWorkflow(data: any): data is SerializedAppState { return data != null && (typeof data === "object") && data.comfyBoxWorkflow; } @@ -271,22 +279,6 @@ export default class ComfyApp { } } - - convertVanillaWorkflow(workflow: ComfyVanillaWorkflow, title: string): SerializedAppState { - const attrs: WorkflowAttributes = { - ...defaultWorkflowAttributes, - title - } - - const canvas: SerializedGraphCanvasState = { - offset: [0, 0], - scale: 1 - } - - const comfyBoxWorkflow = convertVanillaWorkflow(workflow, attrs); - return this.serialize(comfyBoxWorkflow, canvas); - } - saveStateToLocalStorage() { try { uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) @@ -320,9 +312,12 @@ export default class ComfyApp { return false; const workflows = state.workflows as SerializedAppState[]; - for (const workflow of workflows) { - await this.openWorkflow(workflow, defs) - } + await Promise.all(workflows.map(w => { + return this.openWorkflow(w, defs).catch(error => { + console.error("Failed restoring previous workflow", error) + notify(`Failed restoring previous workflow: ${error}`, { type: "error" }) + }) + })); if (typeof state.activeWorkflowIndex === "number") { workflowState.setActiveWorkflow(this.lCanvas, state.activeWorkflowIndex); @@ -566,11 +561,35 @@ export default class ComfyApp { async openWorkflow(data: SerializedAppState, refreshCombos: boolean | Record = true): Promise { if (data.version !== COMFYBOX_SERIAL_VERSION) { - throw `Invalid ComfyBox saved data format: ${data.version}` + const mes = `Invalid ComfyBox saved data format: ${data.version}` + notify(mes, { type: "error" }) + return Promise.reject(mes); } + this.clean(); - const workflow = workflowState.openWorkflow(this.lCanvas, data); + let workflow: ComfyWorkflow; + try { + workflow = workflowState.openWorkflow(this.lCanvas, data); + } + catch (error) { + modalState.pushModal({ + svelteComponent: WorkflowLoadErrorModal, + svelteProps: { + error + } + }) + return Promise.reject(error) + } + + if (workflow.missingNodeTypes.size > 0) { + modalState.pushModal({ + svelteComponent: MissingNodeTypesModal, + svelteProps: { + missingNodeTypes: workflow.missingNodeTypes + } + }) + } // Restore canvas offset/zoom this.lCanvas.deserialize(data.canvas) @@ -586,10 +605,53 @@ export default class ComfyApp { } async openVanillaWorkflow(data: SerializedLGraph, filename: string) { - const converted = this.convertVanillaWorkflow(data, basename(filename)) - console.info("WORKFLWO", converted) - notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" }) - await this.openWorkflow(converted); + const title = basename(filename) + + const attrs: WorkflowAttributes = { + ...defaultWorkflowAttributes, + title + } + + const canvas: SerializedGraphCanvasState = { + offset: [0, 0], + scale: 1 + } + + const [comfyBoxWorkflow, layoutState] = convertVanillaWorkflow(data, attrs); + + const addWorkflow = () => { + notify("Converted ComfyUI workflow to ComfyBox format.", { type: "info" }) + workflowState.addWorkflow(this.lCanvas, comfyBoxWorkflow) + this.lCanvas.deserialize(canvas); + } + + if (comfyBoxWorkflow.missingNodeTypes.size > 0) { + modalState.pushModal({ + svelteComponent: ConfirmConvertWithMissingNodeTypesModal, + svelteProps: { + missingNodeTypes: comfyBoxWorkflow.missingNodeTypes + }, + closeOnClick: false, + showCloseButton: false, + buttons: [ + { + name: "Cancel", + variant: "secondary", + onClick: () => { + layoutStates.remove(comfyBoxWorkflow.id) + } + }, + { + name: "Convert", + variant: "primary", + onClick: addWorkflow + }, + ] + }) + } + else { + addWorkflow() + } } setActiveWorkflow(id: WorkflowInstID) { @@ -840,11 +902,10 @@ export default class ComfyApp { } modalState.pushModal({ title: "A1111 Prompt Details", - svelteComponent: A1111PromptDisplay, + svelteComponent: A1111PromptModal, svelteProps: { prompt: a1111Info }, - showCloseButton: true }) } else { diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte index ad348f2..783bacb 100644 --- a/src/lib/components/ComfyWorkflowsView.svelte +++ b/src/lib/components/ComfyWorkflowsView.svelte @@ -137,12 +137,12 @@ if (!fileInput) return; + fileInput.value = null; fileInput.click(); } function loadWorkflow(): void { app.handleFile(fileInput.files[0]); - fileInput.files = null; } function doSaveLocal(): void { @@ -327,7 +327,7 @@ Error loading app
{error}
- {#if error.stack} + {#if error != null && error.stack} {@const lines = error.stack.split("\n")} {#each lines as line}
{line}
diff --git a/src/lib/components/GlobalModal.svelte b/src/lib/components/GlobalModal.svelte index 14b129c..c5b8c89 100644 --- a/src/lib/components/GlobalModal.svelte +++ b/src/lib/components/GlobalModal.svelte @@ -1,5 +1,5 @@ {#each $modalState.activeModals as modal(modal.id)} - onClose(modal)}> + onClose(modal)}> @@ -26,10 +34,10 @@ {/if} -
+
{#if modal != null && modal.buttons?.length > 0} {#each modal.buttons as button} - {/each} @@ -42,3 +50,9 @@
{/each} + + diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 96cab3f..47f5012 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -50,7 +50,7 @@
- diff --git a/src/lib/components/A1111PromptDisplay.svelte b/src/lib/components/modal/A1111PromptModal.svelte similarity index 93% rename from src/lib/components/A1111PromptDisplay.svelte rename to src/lib/components/modal/A1111PromptModal.svelte index 134ff5b..6716a77 100644 --- a/src/lib/components/A1111PromptDisplay.svelte +++ b/src/lib/components/modal/A1111PromptModal.svelte @@ -1,11 +1,10 @@ + +
+ When loading the graph, the following node types were not found: +
    + {#each Array.from(missingNodeTypes) as type} +
  • {type}
  • + {/each} +
+ Do you want to convert the workflow into ComfyBox format anyway? (You may lose some workflow state) +
+ + diff --git a/src/lib/components/modal/MissingNodeTypesModal.svelte b/src/lib/components/modal/MissingNodeTypesModal.svelte new file mode 100644 index 0000000..53f8326 --- /dev/null +++ b/src/lib/components/modal/MissingNodeTypesModal.svelte @@ -0,0 +1,26 @@ + + +
+ When loading the graph, the following node types were not found: +
    + {#each Array.from(missingNodeTypes) as type} +
  • {type}
  • + {/each} +
+ Nodes that have failed to load will show as red on the graph. +
+ + diff --git a/src/lib/components/modal/WorkflowLoadErrorModal.svelte b/src/lib/components/modal/WorkflowLoadErrorModal.svelte new file mode 100644 index 0000000..812735d --- /dev/null +++ b/src/lib/components/modal/WorkflowLoadErrorModal.svelte @@ -0,0 +1,38 @@ + + +
+ Loading aborted due to error reloading workflow data: +
+
+            {error.toString()}
+        
+
+            {error?.stack || "(No stacktrace available)"}
+        
+
+
+ + diff --git a/src/lib/convertVanillaWorkflow.ts b/src/lib/convertVanillaWorkflow.ts index 50933f0..ddd8075 100644 --- a/src/lib/convertVanillaWorkflow.ts +++ b/src/lib/convertVanillaWorkflow.ts @@ -1,6 +1,6 @@ import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot } from "@litegraph-ts/core"; import type { SerializedAppState } from "./components/ComfyApp"; -import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "./stores/layoutStates"; +import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates"; import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState"; import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas"; import ComfyApp from "./components/ComfyApp"; @@ -158,7 +158,7 @@ function rewriteIDsInGraph(vanillaWorkflow: ComfyVanillaWorkflow) { } } -export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): ComfyWorkflow { +export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] { const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create(); const { root, left, right } = layoutState.initDefaultLayout(); @@ -335,11 +335,5 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork const layout = layoutState.serialize(); comfyBoxWorkflow.deserialize(layoutState, { graph: vanillaWorkflow, attrs, layout }) - for (const node of comfyBoxWorkflow.graph.iterateNodesInOrder()) { - if ((node as any).isBackendNode) { - console.warn("BENDNODE", node) - } - } - - return comfyBoxWorkflow + return [comfyBoxWorkflow, layoutState] } diff --git a/src/lib/stores/modalState.ts b/src/lib/stores/modalState.ts index cf4ab6c..bb22c7e 100644 --- a/src/lib/stores/modalState.ts +++ b/src/lib/stores/modalState.ts @@ -5,16 +5,18 @@ import { v4 as uuidv4 } from "uuid"; export type ModalButton = { name: string, variant: "primary" | "secondary", - onClick: () => void + onClick: () => void, + closeOnClick?: boolean } export interface ModalData { id: string, - title: string, + title?: string, onClose?: () => void, svelteComponent?: typeof SvelteComponentDev, - svelteProps?: Record, - buttons?: ModalButton[], - showCloseButton?: boolean + svelteProps: Record, + buttons: ModalButton[], + showCloseButton: boolean, + closeOnClick: boolean } export interface ModalState { activeModals: ModalData[] @@ -34,7 +36,10 @@ const store: Writable = writable( function pushModal(data: Partial) { const modal: ModalData = { - title: "Modal", + showCloseButton: true, + closeOnClick: true, + buttons: [], + svelteProps: {}, ...data, id: uuidv4(), } diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts index 67ac863..b86d323 100644 --- a/src/lib/stores/workflowState.ts +++ b/src/lib/stores/workflowState.ts @@ -79,7 +79,7 @@ export class ComfyWorkflow { /* * Missing node types encountered when deserializing the graph */ - missingNodeTypes: string[]; + missingNodeTypes: Set = new Set(); get layout(): WritableLayoutStateStore | null { return layoutStates.getLayout(this.id) @@ -168,6 +168,13 @@ export class ComfyWorkflow { } } + /* + * Creates a workflow and layout. + * + * NOTE: The layout will be attached to the global store, but the workflow + * will not. If you change your mind later be sure to call + * layoutStates.remove(workflow.id)! + */ static create(title: string = "New Workflow"): [ComfyWorkflow, WritableLayoutStateStore] { const workflow = new ComfyWorkflow(title); const layoutState = layoutStates.create(workflow); @@ -175,7 +182,7 @@ export class ComfyWorkflow { } deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) { - this.missingNodeTypes = [] + this.missingNodeTypes.clear(); for (let n of data.graph.nodes) { // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now @@ -183,7 +190,7 @@ export class ComfyWorkflow { // Find missing node types if (!(n.type in LiteGraph.registered_node_types)) { - this.missingNodeTypes.push(n.type); + this.missingNodeTypes.add(n.type); } } @@ -233,6 +240,7 @@ type WorkflowStateOps = { getActiveWorkflow: () => ComfyWorkflow | null createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyWorkflow, openWorkflow: (canvas: ComfyGraphCanvas, data: SerializedAppState) => ComfyWorkflow, + addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyWorkflow) => void, closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void, closeAllWorkflows: (canvas: ComfyGraphCanvas) => void, setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number) => ComfyWorkflow | null @@ -296,6 +304,12 @@ function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState): Comfy const [workflow, layoutState] = ComfyWorkflow.create("Workflow") workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout, attrs: data.attrs }) + addWorkflow(canvas, workflow); + + return workflow; +} + +function addWorkflow(canvas: ComfyGraphCanvas, workflow: ComfyWorkflow) { const state = get(store); state.openedWorkflows.push(workflow); state.openedWorkflowsByID[workflow.id] = workflow; @@ -317,7 +331,6 @@ function closeWorkflow(canvas: ComfyGraphCanvas, index: number) { layoutStates.remove(workflow.id) - state.openedWorkflows.splice(index, 1) delete state.openedWorkflowsByID[workflow.id] let newIndex = clamp(index, 0, state.openedWorkflows.length - 1) @@ -372,6 +385,7 @@ const workflowStateStore: WritableWorkflowStateStore = getActiveWorkflow, createNewWorkflow, openWorkflow, + addWorkflow, closeWorkflow, closeAllWorkflows, setActiveWorkflow, diff --git a/src/mobile/GenToolbar.svelte b/src/mobile/GenToolbar.svelte index eaaa917..a4a13ce 100644 --- a/src/mobile/GenToolbar.svelte +++ b/src/mobile/GenToolbar.svelte @@ -41,12 +41,12 @@ return; navigator.vibrate(20) + fileInput.value = null; fileInput.click(); } function loadWorkflow(): void { app.handleFile(fileInput.files[0]); - fileInput.files = null; } function doSaveLocal(): void {