Skip to content

Commit

Permalink
Show workflow templates from custom nodes (#2032)
Browse files Browse the repository at this point in the history
  • Loading branch information
bezo97 authored Dec 30, 2024
1 parent 92a5a80 commit 5218024
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 59 deletions.
75 changes: 75 additions & 0 deletions src/components/templates/TemplateWorkflowCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<template>
<Card :data-testid="`template-workflow-${props.workflowName}`">
<template #header>
<div class="flex items-center justify-center">
<div
class="relative overflow-hidden rounded-lg cursor-pointer w-64 h-64"
>
<img
v-if="!imageError"
:src="
props.moduleName === 'default'
? `templates/${props.workflowName}.jpg`
: `api/workflow_templates/${props.moduleName}/${props.workflowName}.jpg`
"
@error="imageError = true"
class="w-64 h-64 rounded-lg object-cover thumbnail"
/>
<div v-else class="w-64 h-64 content-center text-center">
<i class="pi pi-file" style="font-size: 4rem"></i>
</div>
<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"
class="absolute inset-0 z-1 w-3/12 h-full"
/>
</div>
</div>
</template>
<template #subtitle>
<!--Default templates have translations-->
<template v-if="props.moduleName === 'default'">
{{
$t(
`templateWorkflows.template.${props.workflowName}`,
props.workflowName
)
}}
</template>
<template v-else>
{{ props.workflowName }}
</template>
</template>
</Card>
</template>

<script setup lang="ts">
import Card from 'primevue/card'
import ProgressSpinner from 'primevue/progressspinner'
import { ref } from 'vue'
const props = defineProps<{
moduleName: string
workflowName: string
loading: boolean
}>()
const imageError = ref(false)
</script>

<style lang="css" scoped>
.p-card {
--p-card-body-padding: 10px 0 0 0;
overflow: hidden;
}
:deep(.p-card-subtitle) {
text-align: center;
}
</style>
174 changes: 116 additions & 58 deletions src/components/templates/TemplateWorkflowsContent.vue
Original file line number Diff line number Diff line change
@@ -1,81 +1,139 @@
<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}`"
>
<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>
<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"></i>
</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 class="flex h-96" data-testid="template-workflows-content">
<div class="relative">
<ProgressSpinner
v-if="!workflowTemplatesStore.isLoaded"
class="absolute w-8 h-full inset-0"
/>
<Listbox
:model-value="selectedTab"
@update:model-value="handleTabSelection"
:options="tabs"
optionLabel="title"
scroll-height="auto"
class="overflow-y-auto w-64 h-full"
listStyle="max-height:unset"
/>
</div>
<Carousel
class="carousel justify-center"
:value="selectedTab.templates"
:responsive-options="responsiveOptions"
:numVisible="4"
:numScroll="3"
:key="selectedTab.moduleName"
>
<template #item="slotProps">
<div @click="loadWorkflow(slotProps.data)">
<TemplateWorkflowCard
:moduleName="selectedTab.moduleName"
:workflowName="slotProps.data"
:loading="slotProps.data === workflowLoading"
/>
</div>
</template>
</Carousel>
</div>
</template>

<script setup lang="ts">
import { useDialogStore } from '@/stores/dialogStore'
import Card from 'primevue/card'
import Carousel from 'primevue/carousel'
import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner'
import { ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
import { app } from '@/scripts/app'
import { api } from '@/scripts/api'
import { useI18n } from 'vue-i18n'
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
interface WorkflowTemplatesTab {
moduleName: string
title: string
templates: string[]
}
const { t } = useI18n()
const templates = ['default', 'image2image', 'upscale', 'flux_schnell']
const loading = ref<string | null>(null)
//These default templates are provided by the frontend
const comfyUITemplates: WorkflowTemplatesTab = {
moduleName: 'default',
title: 'ComfyUI',
templates: ['default', 'image2image', 'upscale', 'flux_schnell']
}
const responsiveOptions = ref([
{
breakpoint: '1660px',
numVisible: 3,
numScroll: 2
},
{
breakpoint: '1360px',
numVisible: 2,
numScroll: 1
},
{
breakpoint: '960px',
numVisible: 1,
numScroll: 1
}
])
const workflowTemplatesStore = useWorkflowTemplatesStore()
const selectedTab = ref<WorkflowTemplatesTab>(comfyUITemplates)
const workflowLoading = ref<string | null>(null)
const tabs = computed<WorkflowTemplatesTab[]>(() => {
return [
comfyUITemplates,
...Object.entries(workflowTemplatesStore.items).map(([key, value]) => ({
moduleName: key,
title: key,
templates: value
}))
]
})
onMounted(async () => {
await workflowTemplatesStore.loadWorkflowTemplates()
})
const handleTabSelection = (selection: WorkflowTemplatesTab | null) => {
//Listbox allows deselecting so this special case is ignored here
if (selection !== selectedTab.value && selection !== null)
selectedTab.value = selection
}
const loadWorkflow = async (id: string) => {
loading.value = id
const json = await fetch(api.fileURL(`templates/${id}.json`)).then((r) =>
r.json()
)
workflowLoading.value = id
let json
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.apiURL(
`/workflow_templates/${selectedTab.value.moduleName}/${id}.json`
)
).then((r) => r.json())
}
useDialogStore().closeDialog()
await app.loadGraphData(
json,
true,
true,
t(`templateWorkflows.template.${id}`)
)
const workflowName =
selectedTab.value.moduleName === 'default'
? t(`templateWorkflows.template.${id}`, id)
: id
await app.loadGraphData(json, true, true, workflowName)
return false
}
</script>

<style lang="css" scoped>
.p-card {
--p-card-body-padding: 10px 0 0 0;
overflow: hidden;
}
:deep(.p-card-subtitle) {
text-align: center;
.carousel {
width: 66vw;
}
</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
30 changes: 30 additions & 0 deletions src/stores/workflowTemplatesStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { api } from '@/scripts/api'

export const useWorkflowTemplatesStore = defineStore(
'workflowTemplates',
() => {
const items = ref<{
[customNodesName: string]: string[]
}>({})
const isLoaded = ref(false)

async function loadWorkflowTemplates() {
try {
if (!isLoaded.value) {
items.value = await api.getWorkflowTemplates()
isLoaded.value = true
}
} catch (error) {
console.error('Error fetching workflow templates:', error)
}
}

return {
items,
isLoaded,
loadWorkflowTemplates
}
}
)
6 changes: 5 additions & 1 deletion vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export default defineConfig({
ws: true
},

'/workflow_templates': {
target: DEV_SERVER_COMFYUI_URL
},

'/testsubrouteindex': {
target: 'http://localhost:5173',
rewrite: (path) => path.substring('/testsubrouteindex'.length)
Expand Down Expand Up @@ -183,4 +187,4 @@ export default defineConfig({
'@comfyorg/comfyui-electron-types'
]
}
}) as UserConfigExport
}) as UserConfigExport

0 comments on commit 5218024

Please sign in to comment.