Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 115 additions & 39 deletions src/components/templates/TemplateWorkflowsContent.vue
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}`"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the data-testid for playwright test.

<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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template workflow item here should probably be extracted as a separated Vue component.

@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'
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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`
)
).then((r) => r.json())
}
useDialogStore().closeDialog()
await app.loadGraphData(
json,
true,
true,
t(`templateWorkflows.template.${id}`)
t(`templateWorkflows.template.${id}`, id)
)

return false
Expand All @@ -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: '🗎';
text-align: center;
align-content: center;
font-size: 64px;
}
</style>
11 changes: 11 additions & 0 deletions src/scripts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,17 @@ export class ComfyApi extends EventTarget {
return await resp.json()
}

/**
* Gets the available workflow templates from custom nodes.
* @returns A map of custom_node names and associated template workflow names.
*/
async getWorkflowTemplates(): Promise<{
[customNodesName: string]: string[]
}> {
const res = await this.fetchApi('/workflow_templates')
return await res.json()
}

/**
* Gets a list of embedding names
*/
Expand Down
45 changes: 26 additions & 19 deletions vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'))
)
}

Expand All @@ -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
}
}
}
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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': {
Expand All @@ -111,14 +115,18 @@ export default defineConfig({
res.end(JSON.stringify([]))
}
return null
},
}
},

'/ws': {
target: DEV_SERVER_COMFYUI_URL,
ws: true
},

'/workflow_templates': {
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
Expand All @@ -131,7 +139,7 @@ export default defineConfig({
comfyAPIPlugin(),

Icons({
'compiler': 'vue3'
compiler: 'vue3'
}),

Components({
Expand Down Expand Up @@ -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: {
Expand All @@ -178,9 +188,6 @@ export default defineConfig({
},

optimizeDeps: {
exclude: [
'@comfyorg/litegraph',
'@comfyorg/comfyui-electron-types'
]
exclude: ['@comfyorg/litegraph', '@comfyorg/comfyui-electron-types']
}
}) as UserConfigExport
Loading