Files
ComfyUI_frontend/src/services/workflowService.ts
2025-02-28 22:09:17 -05:00

403 lines
12 KiB
TypeScript

import { LGraphCanvas } from '@comfyorg/litegraph'
import type { Vector2 } from '@comfyorg/litegraph'
import { toRaw } from 'vue'
import { t } from '@/i18n'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
import { downloadBlob } from '@/scripts/utils'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { appendJsonExt } from '@/utils/formatUtil'
import { useDialogService } from './dialogService'
export const useWorkflowService = () => {
const settingStore = useSettingStore()
const workflowStore = useWorkflowStore()
const toastStore = useToastStore()
const dialogService = useDialogService()
async function getFilename(defaultName: string): Promise<string | null> {
if (settingStore.get('Comfy.PromptFilename')) {
let filename = await dialogService.prompt({
title: t('workflowService.exportWorkflow'),
message: t('workflowService.enterFilename') + ':',
defaultValue: defaultName
})
if (!filename) return null
if (!filename.toLowerCase().endsWith('.json')) {
filename += '.json'
}
return filename
}
return defaultName
}
/**
* Export the current workflow as a JSON file
* @param filename The filename to save the workflow as
* @param promptProperty The property of the prompt to export
*/
const exportWorkflow = async (
filename: string,
promptProperty: 'workflow' | 'output'
): Promise<void> => {
const workflow = workflowStore.activeWorkflow
if (workflow?.path) {
filename = workflow.filename
}
const p = await app.graphToPrompt()
const json = JSON.stringify(p[promptProperty], null, 2)
const blob = new Blob([json], { type: 'application/json' })
const file = await getFilename(filename)
if (!file) return
downloadBlob(file, blob)
}
/**
* Save a workflow as a new file
* @param workflow The workflow to save
*/
const saveWorkflowAs = async (workflow: ComfyWorkflow) => {
const newFilename = await dialogService.prompt({
title: t('workflowService.saveWorkflow'),
message: t('workflowService.enterFilename') + ':',
defaultValue: workflow.filename
})
if (!newFilename) return
const newPath = workflow.directory + '/' + appendJsonExt(newFilename)
const newKey = newPath.substring(ComfyWorkflow.basePath.length)
const existingWorkflow = workflowStore.getWorkflowByPath(newPath)
if (existingWorkflow && !existingWorkflow.isTemporary) {
const res = await dialogService.confirm({
title: t('sideToolbar.workflowTab.confirmOverwriteTitle'),
type: 'overwrite',
message: t('sideToolbar.workflowTab.confirmOverwrite'),
itemList: [newPath]
})
if (res !== true) return
if (existingWorkflow.path === workflow.path) {
await saveWorkflow(workflow)
return
}
const deleted = await deleteWorkflow(existingWorkflow, true)
if (!deleted) return
}
if (workflow.isTemporary) {
await renameWorkflow(workflow, newPath)
await workflowStore.saveWorkflow(workflow)
} else {
const tempWorkflow = workflowStore.createTemporary(
newKey,
workflow.activeState as ComfyWorkflowJSON
)
await openWorkflow(tempWorkflow)
await workflowStore.saveWorkflow(tempWorkflow)
}
}
/**
* Save a workflow
* @param workflow The workflow to save
*/
const saveWorkflow = async (workflow: ComfyWorkflow) => {
if (workflow.isTemporary) {
await saveWorkflowAs(workflow)
} else {
await workflowStore.saveWorkflow(workflow)
}
}
/**
* Load the default workflow
*/
const loadDefaultWorkflow = async () => {
await app.loadGraphData(defaultGraph)
}
/**
* Load the tutorial workflow
*/
const loadTutorialWorkflow = async () => {
const tutorialWorkflow = await fetch(
api.fileURL('/templates/default.json')
).then((r) => r.json())
await app.loadGraphData(tutorialWorkflow, false, false, 'tutorial', {
showMissingModelsDialog: true
})
}
/**
* Load a blank workflow
*/
const loadBlankWorkflow = async () => {
await app.loadGraphData(blankGraph)
}
/**
* Reload the current workflow
* This is used to refresh the node definitions update, e.g. when the locale changes.
*/
const reloadCurrentWorkflow = async () => {
const workflow = workflowStore.activeWorkflow
if (workflow) {
await openWorkflow(workflow, { force: true })
}
}
/**
* Open a workflow in the current workspace
* @param workflow The workflow to open
* @param options The options for opening the workflow
*/
const openWorkflow = async (
workflow: ComfyWorkflow,
options: { force: boolean } = { force: false }
) => {
if (workflowStore.isActive(workflow) && !options.force) return
const loadFromRemote = !workflow.isLoaded
if (loadFromRemote) {
await workflow.load()
}
await app.loadGraphData(
toRaw(workflow.activeState) as ComfyWorkflowJSON,
/* clean=*/ true,
/* restore_view=*/ true,
workflow,
{
showMissingModelsDialog: loadFromRemote,
showMissingNodesDialog: loadFromRemote
}
)
}
/**
* Close a workflow with confirmation if there are unsaved changes
* @param workflow The workflow to close
* @returns true if the workflow was closed, false if the user cancelled
*/
const closeWorkflow = async (
workflow: ComfyWorkflow,
options: { warnIfUnsaved: boolean; hint?: string } = {
warnIfUnsaved: true
}
): Promise<boolean> => {
if (workflow.isModified && options.warnIfUnsaved) {
const confirmed = await dialogService.confirm({
title: t('sideToolbar.workflowTab.dirtyCloseTitle'),
type: 'dirtyClose',
message: t('sideToolbar.workflowTab.dirtyClose'),
itemList: [workflow.path],
hint: options.hint
})
// Cancel
if (confirmed === null) return false
if (confirmed === true) {
await saveWorkflow(workflow)
}
}
// If this is the last workflow, create a new default temporary workflow
if (workflowStore.openWorkflows.length === 1) {
await loadDefaultWorkflow()
}
// If this is the active workflow, load the next workflow
if (workflowStore.isActive(workflow)) {
await loadNextOpenedWorkflow()
}
await workflowStore.closeWorkflow(workflow)
return true
}
const renameWorkflow = async (workflow: ComfyWorkflow, newPath: string) => {
await workflowStore.renameWorkflow(workflow, newPath)
}
/**
* Delete a workflow
* @param workflow The workflow to delete
* @returns `true` if the workflow was deleted, `false` if the user cancelled
*/
const deleteWorkflow = async (
workflow: ComfyWorkflow,
silent = false
): Promise<boolean> => {
const bypassConfirm = !settingStore.get('Comfy.Workflow.ConfirmDelete')
let confirmed: boolean | null = bypassConfirm || silent
if (!confirmed) {
confirmed = await dialogService.confirm({
title: t('sideToolbar.workflowTab.confirmDeleteTitle'),
type: 'delete',
message: t('sideToolbar.workflowTab.confirmDelete'),
itemList: [workflow.path]
})
if (!confirmed) return false
}
if (workflowStore.isOpen(workflow)) {
const closed = await closeWorkflow(workflow, {
warnIfUnsaved: !confirmed
})
if (!closed) return false
}
await workflowStore.deleteWorkflow(workflow)
if (!silent) {
toastStore.add({
severity: 'info',
summary: t('sideToolbar.workflowTab.deleted'),
life: 1000
})
}
return true
}
/**
* This method is called before loading a new graph.
* There are 3 major functions that loads a new graph to the graph editor:
* 1. loadGraphData
* 2. loadApiJson
* 3. importA1111
*
* This function is used to save the current workflow states before loading
* a new graph.
*/
const beforeLoadNewGraph = () => {
// Use workspaceStore here as it is patched in unit tests.
const workflowStore = useWorkspaceStore().workflow
const activeWorkflow = workflowStore.activeWorkflow
if (activeWorkflow) {
activeWorkflow.changeTracker.store()
}
}
/**
* Set the active workflow after the new graph is loaded.
*
* The call relationship is
* useWorkflowService().openWorkflow -> app.loadGraphData -> useWorkflowService().afterLoadNewGraph
* app.loadApiJson -> useWorkflowService().afterLoadNewGraph
* app.importA1111 -> useWorkflowService().afterLoadNewGraph
*
* @param value The value to set as the active workflow.
* @param workflowData The initial workflow data loaded to the graph editor.
*/
const afterLoadNewGraph = async (
value: string | ComfyWorkflow | null,
workflowData: ComfyWorkflowJSON
) => {
// Use workspaceStore here as it is patched in unit tests.
const workflowStore = useWorkspaceStore().workflow
if (typeof value === 'string') {
const workflow = workflowStore.getWorkflowByPath(
ComfyWorkflow.basePath + appendJsonExt(value)
)
if (workflow?.isPersisted) {
const loadedWorkflow = await workflowStore.openWorkflow(workflow)
loadedWorkflow.changeTracker.restore()
loadedWorkflow.changeTracker.reset(workflowData)
return
}
}
if (value === null || typeof value === 'string') {
const path = value as string | null
const tempWorkflow = workflowStore.createTemporary(
path ? appendJsonExt(path) : undefined,
workflowData
)
await workflowStore.openWorkflow(tempWorkflow)
return
}
// value is a ComfyWorkflow.
const loadedWorkflow = await workflowStore.openWorkflow(value)
loadedWorkflow.changeTracker.reset(workflowData)
loadedWorkflow.changeTracker.restore()
}
/**
* Insert the given workflow into the current graph editor.
*/
const insertWorkflow = async (
workflow: ComfyWorkflow,
options: { position?: Vector2 } = {}
) => {
const loadedWorkflow = await workflow.load()
const data = loadedWorkflow.initialState
const old = localStorage.getItem('litegrapheditor_clipboard')
// @ts-expect-error: zod issue. Should be fixed after enable ts-strict globally
const graph = new LGraph(data)
const canvasElement = document.createElement('canvas')
const canvas = new LGraphCanvas(canvasElement, graph, {
skip_events: true,
skip_render: true
})
canvas.reroutesEnabled = app.canvas.reroutesEnabled
canvas.selectItems()
canvas.copyToClipboard()
app.canvas.pasteFromClipboard(options)
if (old !== null) {
localStorage.setItem('litegrapheditor_clipboard', old)
}
}
const loadNextOpenedWorkflow = async () => {
const nextWorkflow = workflowStore.openedWorkflowIndexShift(1)
if (nextWorkflow) {
await openWorkflow(nextWorkflow)
}
}
const loadPreviousOpenedWorkflow = async () => {
const previousWorkflow = workflowStore.openedWorkflowIndexShift(-1)
if (previousWorkflow) {
await openWorkflow(previousWorkflow)
}
}
/**
* Takes an existing workflow and duplicates it with a new name
*/
const duplicateWorkflow = async (workflow: ComfyWorkflow) => {
const state = JSON.parse(JSON.stringify(workflow.activeState))
const suffix = workflow.isPersisted ? ' (Copy)' : ''
// Remove the suffix `(2)` or similar
const filename = workflow.filename.replace(/\s*\(\d+\)$/, '') + suffix
await app.loadGraphData(state, true, true, filename)
}
return {
exportWorkflow,
saveWorkflowAs,
saveWorkflow,
loadDefaultWorkflow,
loadBlankWorkflow,
loadTutorialWorkflow,
reloadCurrentWorkflow,
openWorkflow,
closeWorkflow,
renameWorkflow,
deleteWorkflow,
insertWorkflow,
loadNextOpenedWorkflow,
loadPreviousOpenedWorkflow,
duplicateWorkflow,
afterLoadNewGraph,
beforeLoadNewGraph
}
}