-
Notifications
You must be signed in to change notification settings - Fork 395
Show workflow templates from custom nodes #2032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
26a4f67
d09fc63
6bc78f7
70fe90a
76156fc
9adf3d0
3b09513
0c99e15
2430d97
686e024
e8d7099
5835c44
75e0584
d160914
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,68 +1,127 @@ | ||
| <template> | ||
| <div | ||
| class="flex flex-wrap content-around justify-around gap-4 mt-4" | ||
| data-testid="template-workflows-content" | ||
| > | ||
| <div | ||
| v-for="template in templates" | ||
| :key="template" | ||
| :data-testid="`template-workflow-${template}`" | ||
| <div class="flex"> | ||
| <Listbox v-model="selectedTab" :options="tabs" optionLabel="title" /> | ||
| <Carousel | ||
| class="carousel" | ||
| :value="selectedTab.templates" | ||
| :numVisible="4" | ||
| :numScroll="3" | ||
| :key="selectedTab.moduleName" | ||
| > | ||
| <Card> | ||
| <template #header> | ||
| <div | ||
| class="relative overflow-hidden rounded-lg cursor-pointer" | ||
| @click="loadWorkflow(template)" | ||
| > | ||
| <img | ||
| :src="`templates/${template}.jpg`" | ||
| class="w-64 h-64 rounded-lg object-cover" | ||
| /> | ||
| <a> | ||
| <template #item="slotProps"> | ||
| <Card> | ||
| <template #header> | ||
| <div class="flex center justify-center"> | ||
| <div | ||
| class="absolute top-0 left-0 w-64 h-64 overflow-hidden opacity-0 transition duration-300 ease-in-out hover:opacity-100 bg-opacity-50 bg-black flex items-center justify-center" | ||
| class="relative overflow-hidden rounded-lg cursor-pointer w-64 h-64" | ||
|
||
| @click="loadWorkflow(slotProps.data)" | ||
| > | ||
| <i class="pi pi-play-circle"></i> | ||
| <img | ||
| v-if="selectedTab.moduleName === 'default'" | ||
| :src="`templates/${slotProps.data}.jpg`" | ||
| class="w-64 h-64 rounded-lg object-cover thumbnail" | ||
| /> | ||
| <img | ||
| v-else | ||
| :src="`workflow_templates/${selectedTab.moduleName}/${slotProps.data}.jpg`" | ||
| class="w-64 h-64 rounded-lg object-cover thumbnail" | ||
| /> | ||
| <a> | ||
| <div | ||
| class="absolute top-0 left-0 w-64 h-64 overflow-hidden opacity-0 transition duration-300 ease-in-out hover:opacity-100 bg-opacity-50 bg-black flex items-center justify-center" | ||
| > | ||
| <i class="pi pi-play-circle" style="color: white"></i> | ||
| </div> | ||
| </a> | ||
| <ProgressSpinner | ||
| v-if="loading === slotProps.data" | ||
| class="absolute inset-0 z-1 w-3/12 h-full" | ||
| /> | ||
| </div> | ||
| </a> | ||
| <ProgressSpinner | ||
| v-if="loading === template" | ||
| class="absolute inset-0 z-1 w-3/12 h-full" | ||
| /> | ||
| </div> | ||
| </template> | ||
| <template #subtitle>{{ | ||
| $t(`templateWorkflows.template.${template}`) | ||
| }}</template> | ||
| </Card> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| <template #subtitle>{{ slotProps.data }}</template> | ||
| </Card> | ||
| </template> | ||
| </Carousel> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { useDialogStore } from '@/stores/dialogStore' | ||
| import Carousel from 'primevue/carousel' | ||
| import Listbox from 'primevue/listbox' | ||
| import Card from 'primevue/card' | ||
| import ProgressSpinner from 'primevue/progressspinner' | ||
| import { ref } from 'vue' | ||
| import { onMounted, ref } from 'vue' | ||
| import { app } from '@/scripts/app' | ||
| import { api } from '@/scripts/api' | ||
| import { useI18n } from 'vue-i18n' | ||
| import toast from 'primevue/toast' | ||
christian-byrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const { t } = useI18n() | ||
|
|
||
| const templates = ['default', 'image2image', 'upscale', 'flux_schnell'] | ||
| const loading = ref<string | null>(null) | ||
|
|
||
| interface WorkflowTemplatesTab { | ||
| moduleName: string | ||
| title: string | ||
| templates: string[] | ||
| } | ||
|
|
||
| //These default templates are provided by the frontend | ||
| const comfyUITemplates = { | ||
| moduleName: 'default', | ||
| title: 'ComfyUI', | ||
| templates: ['default', 'image2image', 'upscale', 'flux_schnell'] | ||
| } | ||
|
|
||
| const tabs = ref<WorkflowTemplatesTab[]>([comfyUITemplates]) | ||
|
|
||
| const selectedTab = ref<WorkflowTemplatesTab>(comfyUITemplates) | ||
|
|
||
| onMounted(async () => { | ||
| try { | ||
| const workflowTemplates = await api.getWorkflowTemplates() | ||
| tabs.value = [ | ||
| comfyUITemplates, | ||
| ...Object.entries(workflowTemplates).map(([key, value]) => ({ | ||
| moduleName: key, | ||
| title: key, | ||
| templates: value | ||
| })) | ||
| ] | ||
| } catch (error) { | ||
| console.error('Error fetching workflow templates:', error) | ||
| toast.add({ | ||
| severity: 'error', | ||
| summary: 'Error', | ||
| detail: 'Failed to fetch workflow templates', | ||
| life: 5000 | ||
| }) | ||
| } | ||
| }) | ||
|
|
||
| const loadWorkflow = async (id: string) => { | ||
| loading.value = id | ||
| const json = await fetch(api.fileURL(`templates/${id}.json`)).then((r) => | ||
| r.json() | ||
| ) | ||
| let json | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's extract the storage of custom nodes workflow templates as a Pinia store to avoid reloading everytime the dialog is opened. |
||
| if (selectedTab.value.moduleName === 'default') { | ||
| // Default templates provided by frontend are served on this separate endpoint | ||
| json = await fetch(api.fileURL(`templates/${id}.json`)).then((r) => | ||
| r.json() | ||
| ) | ||
| } else { | ||
| json = await fetch( | ||
| api.fileURL( | ||
| `workflow_templates/${selectedTab.value.moduleName}/${id}.json` | ||
christian-byrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
| ).then((r) => r.json()) | ||
| } | ||
| useDialogStore().closeDialog() | ||
| await app.loadGraphData( | ||
| json, | ||
| true, | ||
| true, | ||
| t(`templateWorkflows.template.${id}`) | ||
| t(`templateWorkflows.template.${id}`, id) | ||
| ) | ||
|
|
||
| return false | ||
|
|
@@ -78,4 +137,21 @@ const loadWorkflow = async (id: string) => { | |
| :deep(.p-card-subtitle) { | ||
| text-align: center; | ||
| } | ||
|
|
||
| .carousel { | ||
| width: 1300px; | ||
| } | ||
|
|
||
| /* Fallback graphics for workflows that don't have an image. */ | ||
| img.thumbnail::before { | ||
| position: absolute; | ||
| width: 100%; | ||
| height: 100%; | ||
| background-color: var(--comfy-menu-secondary-bg); | ||
| color: var(--fg-color); | ||
| content: '🗎'; | ||
christian-byrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| text-align: center; | ||
| align-content: center; | ||
| font-size: 64px; | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import { defineConfig, Plugin } from 'vite' | |
| import type { UserConfigExport } from 'vitest/config' | ||
| import vue from '@vitejs/plugin-vue' | ||
| import path from 'path' | ||
| import dotenv from "dotenv" | ||
| import dotenv from 'dotenv' | ||
| import Icons from 'unplugin-icons/vite' | ||
| import IconsResolver from 'unplugin-icons/resolver' | ||
| import Components from 'unplugin-vue-components/vite' | ||
|
|
@@ -20,9 +20,9 @@ interface ShimResult { | |
| } | ||
|
|
||
| function isLegacyFile(id: string): boolean { | ||
| return id.endsWith('.ts') && ( | ||
| id.includes("src/extensions/core") || | ||
| id.includes("src/scripts") | ||
| return ( | ||
| id.endsWith('.ts') && | ||
| (id.includes('src/extensions/core') || id.includes('src/scripts')) | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -43,15 +43,15 @@ function comfyAPIPlugin(): Plugin { | |
| const shimComment = `// Shim for ${relativePath}\n` | ||
|
|
||
| this.emitFile({ | ||
| type: "asset", | ||
| type: 'asset', | ||
| fileName: shimFileName, | ||
| source: shimComment + result.exports.join("") | ||
| source: shimComment + result.exports.join('') | ||
| }) | ||
| } | ||
|
|
||
| return { | ||
| code: result.code, | ||
| map: null // If you're not modifying the source map, return null | ||
| map: null // If you're not modifying the source map, return null | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -64,7 +64,8 @@ function transformExports(code: string, id: string): ShimResult { | |
| let newCode = code | ||
|
|
||
| // Regex to match different types of exports | ||
| const regex = /export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g | ||
| const regex = | ||
| /export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g | ||
| let match | ||
|
|
||
| while ((match = regex.exec(code)) !== null) { | ||
|
|
@@ -75,7 +76,9 @@ function transformExports(code: string, id: string): ShimResult { | |
| newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};` | ||
| } | ||
| newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};` | ||
| exports.push(`export const ${name} = window.comfyAPI.${moduleName}.${name};\n`) | ||
| exports.push( | ||
| `export const ${name} = window.comfyAPI.${moduleName}.${name};\n` | ||
| ) | ||
| } | ||
|
|
||
| return { | ||
|
|
@@ -88,18 +91,19 @@ function getModuleName(id: string): string { | |
| // Simple example to derive a module name from the file path | ||
| const parts = id.split('/') | ||
| const fileName = parts[parts.length - 1] | ||
| return fileName.replace(/\.\w+$/, '') // Remove file extension | ||
| return fileName.replace(/\.\w+$/, '') // Remove file extension | ||
| } | ||
|
|
||
| const DEV_SERVER_COMFYUI_URL = process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188' | ||
| const DEV_SERVER_COMFYUI_URL = | ||
| process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188' | ||
|
|
||
| export default defineConfig({ | ||
| base: '', | ||
| server: { | ||
| host: VITE_REMOTE_DEV ? '0.0.0.0' : undefined, | ||
| proxy: { | ||
| '/internal': { | ||
| target: DEV_SERVER_COMFYUI_URL, | ||
| target: DEV_SERVER_COMFYUI_URL | ||
| }, | ||
|
|
||
| '/api': { | ||
|
|
@@ -111,14 +115,18 @@ export default defineConfig({ | |
| res.end(JSON.stringify([])) | ||
| } | ||
| return null | ||
| }, | ||
| } | ||
| }, | ||
|
|
||
| '/ws': { | ||
| target: DEV_SERVER_COMFYUI_URL, | ||
| ws: true | ||
| }, | ||
|
|
||
| '/workflow_templates': { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert unrelated format changes. |
||
| target: DEV_SERVER_COMFYUI_URL | ||
| }, | ||
|
|
||
| '/testsubrouteindex': { | ||
| target: 'http://localhost:5173', | ||
| rewrite: (path) => path.substring('/testsubrouteindex'.length) | ||
|
|
@@ -131,7 +139,7 @@ export default defineConfig({ | |
| comfyAPIPlugin(), | ||
|
|
||
| Icons({ | ||
| 'compiler': 'vue3' | ||
| compiler: 'vue3' | ||
| }), | ||
|
|
||
| Components({ | ||
|
|
@@ -168,7 +176,9 @@ export default defineConfig({ | |
| }, | ||
|
|
||
| define: { | ||
| '__COMFYUI_FRONTEND_VERSION__': JSON.stringify(process.env.npm_package_version) | ||
| __COMFYUI_FRONTEND_VERSION__: JSON.stringify( | ||
| process.env.npm_package_version | ||
| ) | ||
| }, | ||
|
|
||
| resolve: { | ||
|
|
@@ -178,9 +188,6 @@ export default defineConfig({ | |
| }, | ||
|
|
||
| optimizeDeps: { | ||
| exclude: [ | ||
| '@comfyorg/litegraph', | ||
| '@comfyorg/comfyui-electron-types' | ||
| ] | ||
| exclude: ['@comfyorg/litegraph', '@comfyorg/comfyui-electron-types'] | ||
| } | ||
| }) as UserConfigExport | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep the
data-testidfor playwright test.