+
{{ title }}
-
- {{ description }}
-
+
+
+
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
@@ -78,6 +113,7 @@
+
diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts
index 14e5c6768e..9c610dabd6 100644
--- a/src/composables/useTemplateFiltering.ts
+++ b/src/composables/useTemplateFiltering.ts
@@ -2,55 +2,133 @@ import { type Ref, computed, ref } from 'vue'
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
+export type SortOption = 'recommended' | 'alphabetical' | 'newest'
+
export interface TemplateFilterOptions {
searchQuery?: string
+ selectedModels?: string[]
+ selectedSubcategory?: string | null
+ sortBy?: SortOption
}
export function useTemplateFiltering(
templates: Ref
| TemplateInfo[]
) {
const searchQuery = ref('')
+ const selectedModels = ref([])
+ const selectedSubcategory = ref(null)
+ const sortBy = ref('recommended')
const templatesArray = computed(() => {
const templateData = 'value' in templates ? templates.value : templates
return Array.isArray(templateData) ? templateData : []
})
+ // Get unique subcategories (tags) from current templates
+ const availableSubcategories = computed(() => {
+ const subcategorySet = new Set()
+ templatesArray.value.forEach((template) => {
+ template.tags?.forEach((tag) => subcategorySet.add(tag))
+ })
+ return Array.from(subcategorySet).sort()
+ })
+
+ // Get unique models from all current templates (don't filter by subcategory for model list)
+ const availableModels = computed(() => {
+ const modelSet = new Set()
+
+ templatesArray.value.forEach((template) => {
+ template.models?.forEach((model) => modelSet.add(model))
+ })
+ return Array.from(modelSet).sort()
+ })
+
const filteredTemplates = computed(() => {
- const templateData = templatesArray.value
+ let templateData = templatesArray.value
if (templateData.length === 0) {
return []
}
- if (!searchQuery.value.trim()) {
- return templateData
+ // Filter by search query
+ if (searchQuery.value.trim()) {
+ const query = searchQuery.value.toLowerCase().trim()
+ templateData = templateData.filter((template) => {
+ const searchableText = [
+ template.name,
+ template.title,
+ template.description,
+ template.sourceModule,
+ ...(template.tags || [])
+ ]
+ .filter(Boolean)
+ .join(' ')
+ .toLowerCase()
+
+ return searchableText.includes(query)
+ })
}
- const query = searchQuery.value.toLowerCase().trim()
- return templateData.filter((template) => {
- const searchableText = [
- template.name,
- template.description,
- template.sourceModule
- ]
- .filter(Boolean)
- .join(' ')
- .toLowerCase()
+ // Filter by subcategory
+ if (selectedSubcategory.value) {
+ templateData = templateData.filter((template) =>
+ template.tags?.includes(selectedSubcategory.value!)
+ )
+ }
- return searchableText.includes(query)
- })
+ // Filter by selected models
+ if (selectedModels.value.length > 0) {
+ templateData = templateData.filter((template) =>
+ template.models?.some((model) => selectedModels.value.includes(model))
+ )
+ }
+
+ // Sort templates
+ const sortedData = [...templateData]
+ switch (sortBy.value) {
+ case 'alphabetical':
+ sortedData.sort((a, b) =>
+ (a.title || a.name).localeCompare(b.title || b.name)
+ )
+ break
+ case 'newest':
+ sortedData.sort((a, b) => {
+ const dateA = new Date(a.date || '1970-01-01')
+ const dateB = new Date(b.date || '1970-01-01')
+ return dateB.getTime() - dateA.getTime()
+ })
+ break
+ case 'recommended':
+ default:
+ // Keep original order for recommended (assumes templates are already in recommended order)
+ break
+ }
+
+ return sortedData
})
const resetFilters = () => {
searchQuery.value = ''
+ selectedModels.value = []
+ selectedSubcategory.value = null
+ sortBy.value = 'recommended'
+ }
+
+ const resetModelFilters = () => {
+ selectedModels.value = []
}
const filteredCount = computed(() => filteredTemplates.value.length)
return {
searchQuery,
+ selectedModels,
+ selectedSubcategory,
+ sortBy,
+ availableSubcategories,
+ availableModels,
filteredTemplates,
filteredCount,
- resetFilters
+ resetFilters,
+ resetModelFilters
}
}
diff --git a/src/composables/useTemplateWorkflows.ts b/src/composables/useTemplateWorkflows.ts
index f10237a7de..57d8d39a65 100644
--- a/src/composables/useTemplateWorkflows.ts
+++ b/src/composables/useTemplateWorkflows.ts
@@ -41,8 +41,14 @@ export function useTemplateWorkflows() {
*/
const selectFirstTemplateCategory = () => {
if (allTemplateGroups.value.length > 0) {
- const firstCategory = allTemplateGroups.value[0].modules[0]
- selectTemplateCategory(firstCategory)
+ const firstGroup = allTemplateGroups.value[0]
+ if (
+ firstGroup.subcategories.length > 0 &&
+ firstGroup.subcategories[0].modules.length > 0
+ ) {
+ const firstCategory = firstGroup.subcategories[0].modules[0]
+ selectTemplateCategory(firstCategory)
+ }
}
}
@@ -63,9 +69,9 @@ export function useTemplateWorkflows() {
index = ''
) => {
const basePath =
- sourceModule === 'default'
- ? api.fileURL(`/templates/${template.name}`)
- : api.apiURL(`/workflow_templates/${sourceModule}/${template.name}`)
+ // sourceModule === 'default'
+ api.fileURL(`/templates/${template.name}-1`)
+ // : api.apiURL(`/workflow_templates/${sourceModule}/${template.name}`)
const indexSuffix = sourceModule === 'default' && index ? `-${index}` : ''
return `${basePath}${indexSuffix}.${template.mediaSubtype}`
@@ -106,16 +112,21 @@ export function useTemplateWorkflows() {
try {
// Handle "All" category as a special case
if (sourceModule === 'all') {
- // Find "All" category in the ComfyUI Examples group
- const comfyExamplesGroup = allTemplateGroups.value.find(
- (g) =>
- g.label ===
- t('templateWorkflows.category.ComfyUI Examples', 'ComfyUI Examples')
+ // Find "All" category in the USE CASES group
+ const useCasesGroup = allTemplateGroups.value.find(
+ (g) => g.label === t('templateWorkflows.group.useCases', 'USE CASES')
)
- const allCategory = comfyExamplesGroup?.modules.find(
- (m) => m.moduleName === 'all'
+ const allSubcategory = useCasesGroup?.subcategories.find(
+ (s) =>
+ s.label ===
+ t('templateWorkflows.subcategory.allTemplates', 'All Templates')
+ )
+ const allCategory = allSubcategory?.modules.find(
+ (m: WorkflowTemplates) => m.moduleName === 'all'
+ )
+ const template = allCategory?.templates.find(
+ (t: TemplateInfo) => t.name === id
)
- const template = allCategory?.templates.find((t) => t.name === id)
if (!template || !template.sourceModule) return false
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 3d4d770a6b..20720e270a 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -552,6 +552,15 @@
"title": "Get Started with a Template",
"loadingMore": "Loading more templates...",
"searchPlaceholder": "Search templates...",
+ "modelFilter": "Model",
+ "sort": "Sort",
+ "sortRecommended": "Recommended",
+ "sortAlphabetical": "Alphabetical",
+ "sortNewest": "Newest",
+ "modelsSelected": "{count} models",
+ "allSubcategories": "All",
+ "tutorial": "Tutorial",
+ "useTemplate": "Use Template",
"category": {
"ComfyUI Examples": "ComfyUI Examples",
"Custom Nodes": "Custom Nodes",
@@ -569,6 +578,26 @@
"LLM API": "LLM API",
"All": "All Templates"
},
+ "group": {
+ "useCases": "USE CASES",
+ "toolsBuilding": "TOOLS & BUILDING",
+ "customCommunity": "CUSTOM & COMMUNITY"
+ },
+ "subcategory": {
+ "allTemplates": "All Templates",
+ "imageCreation": "Image Creation",
+ "videoAnimation": "Video & Animation",
+ "spatial": "3D & Spatial",
+ "audio": "Audio",
+ "advancedApis": "Advanced APIs",
+ "postProcessing": "Post-Processing & Utilities",
+ "customNodes": "Custom Nodes",
+ "communityPicks": "Community Picks"
+ },
+ "view": {
+ "allTemplates": "All Templates",
+ "recentItems": "Recent Items"
+ },
"templateDescription": {
"Basics": {
"default": "Generate images from text prompts.",
diff --git a/src/stores/workflowTemplatesStore.ts b/src/stores/workflowTemplatesStore.ts
index 4143a49074..8b4e0ecf47 100644
--- a/src/stores/workflowTemplatesStore.ts
+++ b/src/stores/workflowTemplatesStore.ts
@@ -1,4 +1,3 @@
-import { groupBy } from 'lodash'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
@@ -101,49 +100,6 @@ export const useWorkflowTemplatesStore = defineStore(
)
})
- // Create an "All" category that combines all templates
- const createAllCategory = () => {
- // First, get core templates with source module added
- const coreTemplatesWithSourceModule = coreTemplates.value.flatMap(
- (category) =>
- // For each template in each category, add the sourceModule and pass through any localized fields
- category.templates.map((template) => {
- // Get localized template with its original category title for i18n lookup
- const localizedTemplate = addLocalizedFieldsToTemplate(
- template,
- category.title
- )
- return {
- ...localizedTemplate,
- sourceModule: category.moduleName
- }
- })
- )
-
- // Now handle custom templates
- const customTemplatesWithSourceModule = Object.entries(
- customTemplates.value
- ).flatMap(([moduleName, templates]) =>
- templates.map((name) => ({
- name,
- mediaType: 'image',
- mediaSubtype: 'jpg',
- description: name,
- sourceModule: moduleName
- }))
- )
-
- return {
- moduleName: 'all',
- title: 'All',
- localizedTitle: st('templateWorkflows.category.All', 'All Templates'),
- templates: [
- ...coreTemplatesWithSourceModule,
- ...customTemplatesWithSourceModule
- ]
- }
- }
-
const groupedTemplates = computed(() => {
// Get regular categories
const allTemplates = [
@@ -168,32 +124,156 @@ export const useWorkflowTemplatesStore = defineStore(
)
]
- // Group templates by their main category
- const groupedByCategory = Object.entries(
- groupBy(allTemplates, (template) =>
- template.moduleName === 'default'
- ? st(
- 'templateWorkflows.category.ComfyUI Examples',
- 'ComfyUI Examples'
- )
- : st('templateWorkflows.category.Custom Nodes', 'Custom Nodes')
- )
- ).map(([label, modules]) => ({ label, modules }))
+ // Create subcategories based on template types and content
- // Insert the "All" category at the top of the "ComfyUI Examples" group
- const comfyExamplesGroupIndex = groupedByCategory.findIndex(
- (group) =>
- group.label ===
- st('templateWorkflows.category.ComfyUI Examples', 'ComfyUI Examples')
+ // USE CASES SUBCATEGORIES
+ const imageCreationTemplates = allTemplates.filter((template) =>
+ ['Basics', 'Flux', 'Image'].includes(template.title)
)
- if (comfyExamplesGroupIndex !== -1) {
- groupedByCategory[comfyExamplesGroupIndex].modules.unshift(
- createAllCategory()
- )
+ const videoAnimationTemplates = allTemplates.filter(
+ (template) => template.title === 'Video'
+ )
+
+ const spatialTemplates = allTemplates.filter((template) =>
+ ['3D', 'Area Composition'].includes(template.title)
+ )
+
+ const audioTemplates = allTemplates.filter(
+ (template) => template.title === 'Audio'
+ )
+
+ // TOOLS & BUILDING SUBCATEGORIES
+ const advancedApisTemplates = allTemplates.filter((template) =>
+ ['Image API', 'Video API', '3D API', 'LLM API'].includes(template.title)
+ )
+
+ const postProcessingTemplates = allTemplates.filter((template) =>
+ ['Upscaling', 'ControlNet'].includes(template.title)
+ )
+
+ // CUSTOM & COMMUNITY SUBCATEGORIES
+ const customNodesTemplates = allTemplates.filter(
+ (template) =>
+ template.moduleName !== 'default' &&
+ !advancedApisTemplates.includes(template)
+ )
+
+ const communityPicksTemplates: WorkflowTemplates[] = [] // This could be populated with featured community content
+
+ const groups: TemplateGroup[] = []
+
+ // USE CASES GROUP
+ const useCasesSubcategories = []
+
+ if (imageCreationTemplates.length > 0) {
+ useCasesSubcategories.push({
+ label: st(
+ 'templateWorkflows.subcategory.imageCreation',
+ 'Image Creation'
+ ),
+ modules: imageCreationTemplates
+ })
}
- return groupedByCategory
+ if (videoAnimationTemplates.length > 0) {
+ useCasesSubcategories.push({
+ label: st(
+ 'templateWorkflows.subcategory.videoAnimation',
+ 'Video & Animation'
+ ),
+ modules: videoAnimationTemplates
+ })
+ }
+
+ if (spatialTemplates.length > 0) {
+ useCasesSubcategories.push({
+ label: st('templateWorkflows.subcategory.spatial', '3D & Spatial'),
+ modules: spatialTemplates
+ })
+ }
+
+ if (audioTemplates.length > 0) {
+ useCasesSubcategories.push({
+ label: st('templateWorkflows.subcategory.audio', 'Audio'),
+ modules: audioTemplates
+ })
+ }
+
+ if (useCasesSubcategories.length > 0) {
+ groups.push({
+ label: st('templateWorkflows.group.useCases', 'USE CASES'),
+ subcategories: useCasesSubcategories
+ })
+ }
+
+ // TOOLS & BUILDING GROUP
+ const toolsBuildingSubcategories = []
+
+ if (advancedApisTemplates.length > 0) {
+ toolsBuildingSubcategories.push({
+ label: st(
+ 'templateWorkflows.subcategory.advancedApis',
+ 'Advanced APIs'
+ ),
+ modules: advancedApisTemplates
+ })
+ }
+
+ if (postProcessingTemplates.length > 0) {
+ toolsBuildingSubcategories.push({
+ label: st(
+ 'templateWorkflows.subcategory.postProcessing',
+ 'Post-Processing & Utilities'
+ ),
+ modules: postProcessingTemplates
+ })
+ }
+
+ if (toolsBuildingSubcategories.length > 0) {
+ groups.push({
+ label: st(
+ 'templateWorkflows.group.toolsBuilding',
+ 'TOOLS & BUILDING'
+ ),
+ subcategories: toolsBuildingSubcategories
+ })
+ }
+
+ // CUSTOM & COMMUNITY GROUP
+ const customCommunitySubcategories = []
+
+ if (customNodesTemplates.length > 0) {
+ customCommunitySubcategories.push({
+ label: st(
+ 'templateWorkflows.subcategory.customNodes',
+ 'Custom Nodes'
+ ),
+ modules: customNodesTemplates
+ })
+ }
+
+ if (communityPicksTemplates.length > 0) {
+ customCommunitySubcategories.push({
+ label: st(
+ 'templateWorkflows.subcategory.communityPicks',
+ 'Community Picks'
+ ),
+ modules: communityPicksTemplates
+ })
+ }
+
+ if (customCommunitySubcategories.length > 0) {
+ groups.push({
+ label: st(
+ 'templateWorkflows.group.customCommunity',
+ 'CUSTOM & COMMUNITY'
+ ),
+ subcategories: customCommunitySubcategories
+ })
+ }
+
+ return groups
})
async function loadWorkflowTemplates() {
diff --git a/src/types/workflowTemplateTypes.ts b/src/types/workflowTemplateTypes.ts
index 9ff4e92730..de9918a5e6 100644
--- a/src/types/workflowTemplateTypes.ts
+++ b/src/types/workflowTemplateTypes.ts
@@ -12,16 +12,25 @@ export interface TemplateInfo {
localizedTitle?: string
localizedDescription?: string
sourceModule?: string
+ tags?: string[]
+ models?: string[]
+ date?: string
}
export interface WorkflowTemplates {
moduleName: string
templates: TemplateInfo[]
title: string
+ localizedTitle?: string
+}
+
+export interface TemplateSubcategory {
+ label: string
+ modules: WorkflowTemplates[]
}
export interface TemplateGroup {
label: string
icon?: string
- modules: WorkflowTemplates[]
+ subcategories: TemplateSubcategory[]
}
diff --git a/vite.config.mts b/vite.config.mts
index f7d5279811..585da144be 100644
--- a/vite.config.mts
+++ b/vite.config.mts
@@ -49,7 +49,7 @@ export default defineConfig({
},
'/workflow_templates': {
- target: DEV_SERVER_COMFYUI_URL
+ target: 'http://localhost:1238'
},
// Proxy extension assets (images/videos) under /extensions to the ComfyUI backend
@@ -67,7 +67,7 @@ export default defineConfig({
...(!DISABLE_TEMPLATES_PROXY
? {
'/templates': {
- target: DEV_SERVER_COMFYUI_URL
+ target: 'http://localhost:1238'
}
}
: {}),