From ac8b108f39d2e1439fe7a9e5012267e9257de309 Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 17:17:13 +0100
Subject: [PATCH 01/79] feat: Enhance template filtering with Fuse.js and add
model selection
---
src/composables/useTemplateFiltering.ts | 110 ++++-
.../useWorkflowTemplateSelectorDialog.ts | 29 ++
src/stores/workflowTemplatesStore.ts | 398 +++++++++++++++---
src/stores/workflowTemplatesStoreOld.ts | 218 ++++++++++
src/types/workflowTemplateTypes.ts | 6 +
5 files changed, 678 insertions(+), 83 deletions(-)
create mode 100644 src/composables/useWorkflowTemplateSelectorDialog.ts
create mode 100644 src/stores/workflowTemplatesStoreOld.ts
diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts
index 14e5c6768e..c9f2bc1a6b 100644
--- a/src/composables/useTemplateFiltering.ts
+++ b/src/composables/useTemplateFiltering.ts
@@ -1,56 +1,128 @@
+import Fuse from 'fuse.js'
import { type Ref, computed, ref } from 'vue'
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
export interface TemplateFilterOptions {
searchQuery?: string
+ selectedModels?: string[]
+ sortBy?: 'recommended' | 'alphabetical' | 'newest'
}
export function useTemplateFiltering(
templates: Ref | TemplateInfo[]
) {
const searchQuery = ref('')
+ const selectedModels = ref([])
+ const sortBy = ref<'recommended' | 'alphabetical' | 'newest'>('recommended')
const templatesArray = computed(() => {
const templateData = 'value' in templates ? templates.value : templates
return Array.isArray(templateData) ? templateData : []
})
- const filteredTemplates = computed(() => {
- const templateData = templatesArray.value
- if (templateData.length === 0) {
- return []
- }
+ // Fuse.js configuration for fuzzy search
+ const fuseOptions = {
+ keys: [
+ { name: 'name', weight: 0.3 },
+ { name: 'title', weight: 0.3 },
+ { name: 'description', weight: 0.2 },
+ { name: 'tags', weight: 0.1 },
+ { name: 'models', weight: 0.1 }
+ ],
+ threshold: 0.4,
+ includeScore: true,
+ includeMatches: true
+ }
+
+ const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions))
+
+ const availableModels = computed(() => {
+ const modelSet = new Set()
+ templatesArray.value.forEach((template) => {
+ if (template.models && Array.isArray(template.models)) {
+ template.models.forEach((model) => modelSet.add(model))
+ }
+ })
+ return Array.from(modelSet).sort()
+ })
+ const filteredBySearch = computed(() => {
if (!searchQuery.value.trim()) {
- return templateData
+ return templatesArray.value
+ }
+
+ const results = fuse.value.search(searchQuery.value)
+ return results.map((result) => result.item)
+ })
+
+ const filteredByModels = computed(() => {
+ if (selectedModels.value.length === 0) {
+ return filteredBySearch.value
}
- const query = searchQuery.value.toLowerCase().trim()
- return templateData.filter((template) => {
- const searchableText = [
- template.name,
- template.description,
- template.sourceModule
- ]
- .filter(Boolean)
- .join(' ')
- .toLowerCase()
-
- return searchableText.includes(query)
+ return filteredBySearch.value.filter((template) => {
+ if (!template.models || !Array.isArray(template.models)) {
+ return false
+ }
+ return selectedModels.value.some((selectedModel) =>
+ template.models?.includes(selectedModel)
+ )
})
})
+ const sortedTemplates = computed(() => {
+ const templates = [...filteredByModels.value]
+
+ switch (sortBy.value) {
+ case 'alphabetical':
+ return templates.sort((a, b) => {
+ const nameA = a.title || a.name || ''
+ const nameB = b.title || b.name || ''
+ return nameA.localeCompare(nameB)
+ })
+ case 'newest':
+ return templates.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()
+ })
+ case 'recommended':
+ default:
+ // Keep original order (recommended order)
+ return templates
+ }
+ })
+
+ const filteredTemplates = computed(() => sortedTemplates.value)
+
const resetFilters = () => {
searchQuery.value = ''
+ selectedModels.value = []
+ sortBy.value = 'recommended'
+ }
+
+ const removeModelFilter = (model: string) => {
+ selectedModels.value = selectedModels.value.filter((m) => m !== model)
}
const filteredCount = computed(() => filteredTemplates.value.length)
+ const totalCount = computed(() => templatesArray.value.length)
return {
+ // State
searchQuery,
+ selectedModels,
+ sortBy,
+
+ // Computed
filteredTemplates,
+ availableModels,
filteredCount,
- resetFilters
+ totalCount,
+
+ // Methods
+ resetFilters,
+ removeModelFilter
}
}
diff --git a/src/composables/useWorkflowTemplateSelectorDialog.ts b/src/composables/useWorkflowTemplateSelectorDialog.ts
new file mode 100644
index 0000000000..bcb944a46e
--- /dev/null
+++ b/src/composables/useWorkflowTemplateSelectorDialog.ts
@@ -0,0 +1,29 @@
+import WorkflowTemplateSelector from '@/components/custom/widget/WorkflowTemplateSelector.vue'
+import { useDialogService } from '@/services/dialogService'
+import { useDialogStore } from '@/stores/dialogStore'
+
+const DIALOG_KEY = 'global-workflow-template-selector'
+
+export const useWorkflowTemplateSelectorDialog = () => {
+ const dialogService = useDialogService()
+ const dialogStore = useDialogStore()
+
+ function hide() {
+ dialogStore.closeDialog({ key: DIALOG_KEY })
+ }
+
+ function show() {
+ dialogService.showLayoutDialog({
+ key: DIALOG_KEY,
+ component: WorkflowTemplateSelector,
+ props: {
+ onClose: hide
+ }
+ })
+ }
+
+ return {
+ show,
+ hide
+ }
+}
diff --git a/src/stores/workflowTemplatesStore.ts b/src/stores/workflowTemplatesStore.ts
index 08220e004d..f190d39522 100644
--- a/src/stores/workflowTemplatesStore.ts
+++ b/src/stores/workflowTemplatesStore.ts
@@ -1,9 +1,10 @@
-import { groupBy } from 'es-toolkit/compat'
+import Fuse from 'fuse.js'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
-import { st } from '@/i18n'
+import { i18n, st } from '@/i18n'
import { api } from '@/scripts/api'
+import type { NavGroupData, NavItemData } from '@/types/navTypes'
import type {
TemplateGroup,
TemplateInfo,
@@ -11,13 +12,16 @@ import type {
} from '@/types/workflowTemplateTypes'
import { normalizeI18nKey } from '@/utils/formatUtil'
-const SHOULD_SORT_CATEGORIES = new Set([
- // API Node templates should be strictly sorted by name to avoid any
- // favoritism or bias towards a particular API. Other categories can
- // have their ordering specified in index.json freely.
- 'Image API',
- 'Video API'
-])
+// Enhanced template interface for easier filtering
+interface EnhancedTemplate extends TemplateInfo {
+ sourceModule: string
+ category?: string
+ categoryType?: string
+ isAPI?: boolean
+ isPerformance?: boolean
+ isMacCompatible?: boolean
+ searchableText?: string
+}
export const useWorkflowTemplatesStore = defineStore(
'workflowTemplates',
@@ -26,37 +30,6 @@ export const useWorkflowTemplatesStore = defineStore(
const coreTemplates = shallowRef([])
const isLoaded = ref(false)
- /**
- * Sort a list of templates in alphabetical order by localized display name.
- */
- const sortTemplateList = (templates: TemplateInfo[]) =>
- templates.sort((a, b) => {
- const aName = st(
- `templateWorkflows.name.${normalizeI18nKey(a.name)}`,
- a.title ?? a.name
- )
- const bName = st(
- `templateWorkflows.name.${normalizeI18nKey(b.name)}`,
- b.name
- )
- return aName.localeCompare(bName)
- })
-
- /**
- * Sort any template categories (grouped templates) that should be sorted.
- * Leave other categories' templates in their original order specified in index.json
- */
- const sortCategoryTemplates = (categories: WorkflowTemplates[]) =>
- categories.map((category) => {
- if (SHOULD_SORT_CATEGORIES.has(category.title)) {
- return {
- ...category,
- templates: sortTemplateList(category.templates)
- }
- }
- return category
- })
-
/**
* Add localization fields to a template.
*/
@@ -144,12 +117,13 @@ export const useWorkflowTemplatesStore = defineStore(
}
}
+ /**
+ * Original grouped templates for backward compatibility
+ */
const groupedTemplates = computed(() => {
// Get regular categories
const allTemplates = [
- ...sortCategoryTemplates(coreTemplates.value).map(
- localizeTemplateCategory
- ),
+ ...coreTemplates.value.map(localizeTemplateCategory),
...Object.entries(customTemplates.value).map(
([moduleName, templates]) => ({
moduleName,
@@ -169,38 +143,330 @@ 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 }))
+ const groupedByCategory = [
+ {
+ label: st(
+ 'templateWorkflows.category.ComfyUI Examples',
+ 'ComfyUI Examples'
+ ),
+ modules: [
+ createAllCategory(),
+ ...allTemplates.filter((t) => t.moduleName === 'default')
+ ]
+ },
+ ...(Object.keys(customTemplates.value).length > 0
+ ? [
+ {
+ label: st(
+ 'templateWorkflows.category.Custom Nodes',
+ 'Custom Nodes'
+ ),
+ modules: allTemplates.filter((t) => t.moduleName !== 'default')
+ }
+ ]
+ : [])
+ ]
+
+ return groupedByCategory
+ })
+
+ /**
+ * Enhanced templates with proper categorization for filtering
+ */
+ const enhancedTemplates = computed(() => {
+ const allTemplates: EnhancedTemplate[] = []
+
+ // Process core templates
+ coreTemplates.value.forEach((category) => {
+ category.templates.forEach((template) => {
+ const isAPI = category.title?.includes('API') || false
+ const isPerformance =
+ template.models?.some(
+ (model) =>
+ model.toLowerCase().includes('turbo') ||
+ model.toLowerCase().includes('fast') ||
+ model.toLowerCase().includes('schnell') ||
+ model.toLowerCase().includes('fp8')
+ ) || false
+
+ const isMacCompatible =
+ template.models?.some(
+ (model) =>
+ model.toLowerCase().includes('fp8') ||
+ model.toLowerCase().includes('turbo') ||
+ model.toLowerCase().includes('schnell')
+ ) || false
- // 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')
+ const enhancedTemplate: EnhancedTemplate = {
+ ...template,
+ sourceModule: category.moduleName,
+ category: category.title,
+ categoryType: category.type,
+ isAPI,
+ isPerformance,
+ isMacCompatible,
+ searchableText: [
+ template.title || template.name,
+ template.description || '',
+ category.title,
+ ...(template.tags || []),
+ ...(template.models || [])
+ ].join(' ')
+ }
+
+ allTemplates.push(enhancedTemplate)
+ })
+ })
+
+ // Process custom templates
+ Object.entries(customTemplates.value).forEach(
+ ([moduleName, templates]) => {
+ templates.forEach((name) => {
+ const enhancedTemplate: EnhancedTemplate = {
+ name,
+ title: name,
+ description: name,
+ mediaType: 'image',
+ mediaSubtype: 'jpg',
+ sourceModule: moduleName,
+ category: 'Extensions',
+ categoryType: 'extension',
+ isAPI: false,
+ isPerformance: false,
+ isMacCompatible: false,
+ searchableText: `${name} ${moduleName} extension`
+ }
+ allTemplates.push(enhancedTemplate)
+ })
+ }
)
- if (comfyExamplesGroupIndex !== -1) {
- groupedByCategory[comfyExamplesGroupIndex].modules.unshift(
- createAllCategory()
- )
+ return allTemplates
+ })
+
+ /**
+ * Fuse.js instance for advanced template searching and filtering
+ */
+ const templateFuse = computed(() => {
+ const fuseOptions = {
+ keys: [
+ { name: 'searchableText', weight: 0.4 },
+ { name: 'title', weight: 0.3 },
+ { name: 'name', weight: 0.2 },
+ { name: 'tags', weight: 0.1 }
+ ],
+ threshold: 0.3,
+ includeScore: true
}
- return groupedByCategory
+ return new Fuse(enhancedTemplates.value, fuseOptions)
+ })
+
+ /**
+ * Filter templates by category using Fuse.js
+ */
+ const filterTemplatesByCategory = (categoryId: string) => {
+ if (categoryId === 'all') {
+ return enhancedTemplates.value
+ }
+
+ switch (categoryId) {
+ case 'getting-started':
+ return enhancedTemplates.value.filter((t) => t.category === 'Basics')
+
+ case 'generation-image':
+ return enhancedTemplates.value.filter(
+ (t) => t.categoryType === 'image' && !t.isAPI
+ )
+
+ case 'generation-video':
+ return enhancedTemplates.value.filter(
+ (t) => t.categoryType === 'video' && !t.isAPI
+ )
+
+ case 'generation-3d':
+ return enhancedTemplates.value.filter(
+ (t) => t.categoryType === '3d' && !t.isAPI
+ )
+
+ case 'generation-audio':
+ return enhancedTemplates.value.filter(
+ (t) => t.categoryType === 'audio' && !t.isAPI
+ )
+
+ case 'api-nodes':
+ return enhancedTemplates.value.filter((t) => t.isAPI)
+
+ case 'extensions':
+ return enhancedTemplates.value.filter(
+ (t) => t.sourceModule !== 'default'
+ )
+
+ case 'performance-small':
+ return enhancedTemplates.value.filter((t) => t.isPerformance)
+
+ case 'performance-mac':
+ return enhancedTemplates.value.filter((t) => t.isMacCompatible)
+
+ default:
+ return enhancedTemplates.value
+ }
+ }
+
+ /**
+ * New navigation structure matching NavItemData | NavGroupData format
+ */
+ const navGroupedTemplates = computed<(NavItemData | NavGroupData)[]>(() => {
+ if (!isLoaded.value) return []
+
+ const items: (NavItemData | NavGroupData)[] = []
+
+ // Count templates for each category
+ const imageCounts = enhancedTemplates.value.filter(
+ (t) => t.categoryType === 'image' && !t.isAPI
+ ).length
+ const videoCounts = enhancedTemplates.value.filter(
+ (t) => t.categoryType === 'video' && !t.isAPI
+ ).length
+ const audioCounts = enhancedTemplates.value.filter(
+ (t) => t.categoryType === 'audio' && !t.isAPI
+ ).length
+ const threeDCounts = enhancedTemplates.value.filter(
+ (t) => t.categoryType === '3d' && !t.isAPI
+ ).length
+ const apiCounts = enhancedTemplates.value.filter((t) => t.isAPI).length
+ const gettingStartedCounts = enhancedTemplates.value.filter(
+ (t) => t.category === 'Basics'
+ ).length
+ const extensionCounts = enhancedTemplates.value.filter(
+ (t) => t.sourceModule !== 'default'
+ ).length
+ const performanceCounts = enhancedTemplates.value.filter(
+ (t) => t.isPerformance
+ ).length
+ const macCompatibleCounts = enhancedTemplates.value.filter(
+ (t) => t.isMacCompatible
+ ).length
+
+ // All Templates - as a simple selector
+ items.push({
+ id: 'all',
+ label: st('templateWorkflows.category.All', 'All Templates')
+ })
+
+ // Getting Started - as a simple selector
+ if (gettingStartedCounts > 0) {
+ items.push({
+ id: 'getting-started',
+ label: st(
+ 'templateWorkflows.category.GettingStarted',
+ 'Getting Started'
+ )
+ })
+ }
+
+ // Generation Type - as a group with sub-items
+ if (
+ imageCounts > 0 ||
+ videoCounts > 0 ||
+ threeDCounts > 0 ||
+ audioCounts > 0
+ ) {
+ const generationTypeItems: NavItemData[] = []
+
+ if (imageCounts > 0) {
+ generationTypeItems.push({
+ id: 'generation-image',
+ label: st('templateWorkflows.category.Image', 'Image')
+ })
+ }
+
+ if (videoCounts > 0) {
+ generationTypeItems.push({
+ id: 'generation-video',
+ label: st('templateWorkflows.category.Video', 'Video')
+ })
+ }
+
+ if (threeDCounts > 0) {
+ generationTypeItems.push({
+ id: 'generation-3d',
+ label: st('templateWorkflows.category.3DModels', '3D Models')
+ })
+ }
+
+ if (audioCounts > 0) {
+ generationTypeItems.push({
+ id: 'generation-audio',
+ label: st('templateWorkflows.category.Audio', 'Audio')
+ })
+ }
+
+ items.push({
+ title: st(
+ 'templateWorkflows.category.GenerationType',
+ 'Generation Type'
+ ),
+ items: generationTypeItems
+ })
+ }
+
+ // Closed Models (API nodes) - as a group
+ if (apiCounts > 0) {
+ items.push({
+ title: st('templateWorkflows.category.ClosedModels', 'Closed Models'),
+ items: [
+ {
+ id: 'api-nodes',
+ label: st('templateWorkflows.category.APINodes', 'API nodes')
+ }
+ ]
+ })
+ }
+
+ // Extensions - as a simple selector
+ if (extensionCounts > 0) {
+ items.push({
+ id: 'extensions',
+ label: st('templateWorkflows.category.Extensions', 'Extensions')
+ })
+ }
+
+ // Performance - as a group
+ if (performanceCounts > 0) {
+ const performanceItems: NavItemData[] = [
+ {
+ id: 'performance-small',
+ label: st('templateWorkflows.category.SmallModels', 'Small Models')
+ }
+ ]
+
+ // Mac compatibility (only if there are compatible models)
+ if (macCompatibleCounts > 0) {
+ performanceItems.push({
+ id: 'performance-mac',
+ label: st(
+ 'templateWorkflows.category.RunsOnMac',
+ 'Runs on Mac (Silicon)'
+ )
+ })
+ }
+
+ items.push({
+ title: st('templateWorkflows.category.Performance', 'Performance'),
+ items: performanceItems
+ })
+ }
+
+ return items
})
async function loadWorkflowTemplates() {
try {
if (!isLoaded.value) {
customTemplates.value = await api.getWorkflowTemplates()
- coreTemplates.value = await api.getCoreWorkflowTemplates()
+ const locale = i18n.global.locale.value
+ coreTemplates.value = await api.getCoreWorkflowTemplates(locale)
isLoaded.value = true
}
} catch (error) {
@@ -210,6 +476,10 @@ export const useWorkflowTemplatesStore = defineStore(
return {
groupedTemplates,
+ navGroupedTemplates,
+ enhancedTemplates,
+ templateFuse,
+ filterTemplatesByCategory,
isLoaded,
loadWorkflowTemplates
}
diff --git a/src/stores/workflowTemplatesStoreOld.ts b/src/stores/workflowTemplatesStoreOld.ts
new file mode 100644
index 0000000000..79172e1886
--- /dev/null
+++ b/src/stores/workflowTemplatesStoreOld.ts
@@ -0,0 +1,218 @@
+import { groupBy } from 'es-toolkit/compat'
+import { defineStore } from 'pinia'
+import { computed, ref, shallowRef } from 'vue'
+
+import { i18n, st } from '@/i18n'
+import { api } from '@/scripts/api'
+import type {
+ TemplateGroup,
+ TemplateInfo,
+ WorkflowTemplates
+} from '@/types/workflowTemplateTypes'
+import { normalizeI18nKey } from '@/utils/formatUtil'
+
+const SHOULD_SORT_CATEGORIES = new Set([
+ // API Node templates should be strictly sorted by name to avoid any
+ // favoritism or bias towards a particular API. Other categories can
+ // have their ordering specified in index.json freely.
+ 'Image API',
+ 'Video API'
+])
+
+export const useWorkflowTemplatesStore = defineStore(
+ 'workflowTemplates',
+ () => {
+ const customTemplates = shallowRef<{ [moduleName: string]: string[] }>({})
+ const coreTemplates = shallowRef([])
+ const isLoaded = ref(false)
+
+ /**
+ * Sort a list of templates in alphabetical order by localized display name.
+ */
+ const sortTemplateList = (templates: TemplateInfo[]) =>
+ templates.sort((a, b) => {
+ const aName = st(
+ `templateWorkflows.name.${normalizeI18nKey(a.name)}`,
+ a.title ?? a.name
+ )
+ const bName = st(
+ `templateWorkflows.name.${normalizeI18nKey(b.name)}`,
+ b.name
+ )
+ return aName.localeCompare(bName)
+ })
+
+ /**
+ * Sort any template categories (grouped templates) that should be sorted.
+ * Leave other categories' templates in their original order specified in index.json
+ */
+ const sortCategoryTemplates = (categories: WorkflowTemplates[]) =>
+ categories.map((category) => {
+ if (SHOULD_SORT_CATEGORIES.has(category.title)) {
+ return {
+ ...category,
+ templates: sortTemplateList(category.templates)
+ }
+ }
+ return category
+ })
+
+ /**
+ * Add localization fields to a template.
+ */
+ const addLocalizedFieldsToTemplate = (
+ template: TemplateInfo,
+ categoryTitle: string
+ ) => ({
+ ...template,
+ localizedTitle: st(
+ `templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
+ template.title ?? template.name
+ ),
+ localizedDescription: st(
+ `templateWorkflows.templateDescription.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
+ template.description
+ )
+ })
+
+ /**
+ * Add localization fields to all templates in a list of templates.
+ */
+ const localizeTemplateList = (
+ templates: TemplateInfo[],
+ categoryTitle: string
+ ) =>
+ templates.map((template) =>
+ addLocalizedFieldsToTemplate(template, categoryTitle)
+ )
+
+ /**
+ * Add localization fields to a template category and all its constituent templates.
+ */
+ const localizeTemplateCategory = (templateCategory: WorkflowTemplates) => ({
+ ...templateCategory,
+ localizedTitle: st(
+ `templateWorkflows.category.${normalizeI18nKey(templateCategory.title)}`,
+ templateCategory.title ?? templateCategory.moduleName
+ ),
+ templates: localizeTemplateList(
+ templateCategory.templates,
+ templateCategory.title
+ )
+ })
+
+ // 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 = [
+ ...sortCategoryTemplates(coreTemplates.value).map(
+ localizeTemplateCategory
+ ),
+ ...Object.entries(customTemplates.value).map(
+ ([moduleName, templates]) => ({
+ moduleName,
+ title: moduleName,
+ localizedTitle: st(
+ `templateWorkflows.category.${normalizeI18nKey(moduleName)}`,
+ moduleName
+ ),
+ templates: templates.map((name) => ({
+ name,
+ mediaType: 'image',
+ mediaSubtype: 'jpg',
+ description: name
+ }))
+ })
+ )
+ ]
+
+ // 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 }))
+
+ // 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')
+ )
+
+ if (comfyExamplesGroupIndex !== -1) {
+ groupedByCategory[comfyExamplesGroupIndex].modules.unshift(
+ createAllCategory()
+ )
+ }
+
+ return groupedByCategory
+ })
+
+ async function loadWorkflowTemplates() {
+ try {
+ if (!isLoaded.value) {
+ customTemplates.value = await api.getWorkflowTemplates()
+ const locale = i18n.global.locale.value
+ coreTemplates.value = await api.getCoreWorkflowTemplates(locale)
+ isLoaded.value = true
+ }
+ } catch (error) {
+ console.error('Error fetching workflow templates:', error)
+ }
+ }
+
+ return {
+ groupedTemplates,
+ isLoaded,
+ loadWorkflowTemplates
+ }
+ }
+)
diff --git a/src/types/workflowTemplateTypes.ts b/src/types/workflowTemplateTypes.ts
index 9ff4e92730..1f62102fbb 100644
--- a/src/types/workflowTemplateTypes.ts
+++ b/src/types/workflowTemplateTypes.ts
@@ -12,12 +12,18 @@ 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
+ category?: string
+ type?: string
}
export interface TemplateGroup {
From d36c868a7e9bd94b4f9de445556ef81b10100e3a Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 17:18:11 +0100
Subject: [PATCH 02/79] feat: Implement workflow template selector dialog and
enhance template filtering options
---
.../widget/WorkflowTemplateSelector.vue | 355 ++++++++++++++++++
src/composables/useCoreCommands.ts | 2 +-
src/locales/en/main.json | 15 +
src/scripts/api.ts | 25 +-
src/services/dialogService.ts | 24 ++
5 files changed, 416 insertions(+), 5 deletions(-)
create mode 100644 src/components/custom/widget/WorkflowTemplateSelector.vue
diff --git a/src/components/custom/widget/WorkflowTemplateSelector.vue b/src/components/custom/widget/WorkflowTemplateSelector.vue
new file mode 100644
index 0000000000..758f403104
--- /dev/null
+++ b/src/components/custom/widget/WorkflowTemplateSelector.vue
@@ -0,0 +1,355 @@
+
+
+
+
+
+
+
+
+ {{
+ $t('templateWorkflows.categories', 'Categories')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $t('templateWorkflows.activeFilters', 'Filters:')
+ }}
+
+
+
+
+
+
+
+
+ {{ $t('templateWorkflows.loading', 'Loading templates...') }}
+
+
+
+
+
+
+ {{ $t('templateWorkflows.noResults', 'No templates found') }}
+
+
+ {{
+ $t(
+ 'templateWorkflows.noResultsHint',
+ 'Try adjusting your search or filters'
+ )
+ }}
+
+
+
+
+
loadTemplate(template)"
+ >
+
+
+
+
+
+
+ showTemplateInfo(template)"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ getTemplateTitle(template, template.sourceModule || 'default')
+ }}
+
+
+ {{
+ getTemplateDescription(
+ template,
+ template.sourceModule || 'default'
+ )
+ }}
+
+
+
+ {{ tag }}
+
+
+
+
+
+
+
+
+
+ {{
+ $t('templateWorkflows.resultsCount', {
+ count: filteredCount,
+ total: totalCount
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts
index 80eeca78f7..82cd3426d0 100644
--- a/src/composables/useCoreCommands.ts
+++ b/src/composables/useCoreCommands.ts
@@ -246,7 +246,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-folder-open',
label: 'Browse Templates',
function: () => {
- dialogService.showTemplateWorkflowsDialog()
+ dialogService.showWorkflowTemplateSelectorDialog()
}
},
{
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index b4c2537082..582f6fc76a 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -882,6 +882,21 @@
"audio_ace_step_1_t2a_song": "ACE Step v1 Text to Song",
"audio_ace_step_1_m2m_editing": "ACE Step v1 M2M Editing"
}
+ },
+ "categories": "Categories",
+ "resetFilters": "Reset Filters",
+ "sorting": "Sort by",
+ "activeFilters": "Filters:",
+ "loading": "Loading templates...",
+ "noResults": "No templates found",
+ "noResultsHint": "Try adjusting your search or filters",
+ "modelFilter": "Model Filter",
+ "modelsSelected": "{count} Models",
+ "resultsCount": "Showing {count} of {total} templates",
+ "sort": {
+ "recommended": "Recommended",
+ "alphabetical": "A → Z",
+ "newest": "Newest"
}
},
"graphCanvasMenu": {
diff --git a/src/scripts/api.ts b/src/scripts/api.ts
index e83eb9f8bf..69a9cb1e75 100644
--- a/src/scripts/api.ts
+++ b/src/scripts/api.ts
@@ -597,11 +597,28 @@ export class ComfyApi extends EventTarget {
/**
* Gets the index of core workflow templates.
+ * @param locale Optional locale code (e.g., 'en', 'fr', 'zh') to load localized templates
*/
- async getCoreWorkflowTemplates(): Promise {
- const res = await axios.get(this.fileURL('/templates/index.json'))
- const contentType = res.headers['content-type']
- return contentType?.includes('application/json') ? res.data : []
+ async getCoreWorkflowTemplates(
+ locale?: string
+ ): Promise {
+ const fileName =
+ locale && locale !== 'en' ? `index.${locale}.json` : 'index.json'
+ try {
+ const res = await axios.get(this.fileURL(`/templates/${fileName}`))
+ const contentType = res.headers['content-type']
+ return contentType?.includes('application/json') ? res.data : []
+ } catch (error) {
+ // Fallback to default English version if localized version doesn't exist
+ if (locale && locale !== 'en') {
+ console.warn(
+ `Localized templates for '${locale}' not found, falling back to English`
+ )
+ return this.getCoreWorkflowTemplates('en')
+ }
+ console.error('Error loading core workflow templates:', error)
+ return []
+ }
}
/**
diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts
index 3f0ca9d9ab..83d8e65446 100644
--- a/src/services/dialogService.ts
+++ b/src/services/dialogService.ts
@@ -1,6 +1,7 @@
import { merge } from 'es-toolkit/compat'
import { Component } from 'vue'
+import WorkflowTemplateSelector from '@/components/custom/widget/WorkflowTemplateSelector.vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
@@ -124,6 +125,28 @@ export const useDialogService = () => {
})
}
+ function showWorkflowTemplateSelectorDialog() {
+ const layoutDefaultProps: DialogComponentProps = {
+ headless: true,
+ modal: true,
+ closable: false,
+ pt: {
+ content: { class: '!px-0' },
+ root: { style: 'width: 90vw; max-width: 1400px; height: 85vh;' }
+ }
+ }
+
+ showLayoutDialog({
+ key: 'global-workflow-template-selector',
+ component: WorkflowTemplateSelector,
+ props: {
+ onClose: () =>
+ dialogStore.closeDialog({ key: 'global-workflow-template-selector' })
+ },
+ dialogComponentProps: layoutDefaultProps
+ })
+ }
+
function showIssueReportDialog(
props: InstanceType['$props']
) {
@@ -470,6 +493,7 @@ export const useDialogService = () => {
showAboutDialog,
showExecutionErrorDialog,
showTemplateWorkflowsDialog,
+ showWorkflowTemplateSelectorDialog,
showIssueReportDialog,
showManagerDialog,
showManagerProgressDialog,
From 8e36c347c2bca6e69556f04556ba75af471e69dc Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 18:04:40 +0100
Subject: [PATCH 03/79] feat: use old template workflow view in workflow
selector
---
.../widget/WorkflowTemplateSelector.vue | 145 +++++-------------
src/composables/useTemplateWorkflows.ts | 2 +-
2 files changed, 40 insertions(+), 107 deletions(-)
diff --git a/src/components/custom/widget/WorkflowTemplateSelector.vue b/src/components/custom/widget/WorkflowTemplateSelector.vue
index 758f403104..0cfac0c509 100644
--- a/src/components/custom/widget/WorkflowTemplateSelector.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelector.vue
@@ -104,93 +104,15 @@
-
-
loadTemplate(template)"
- >
-
-
-
-
-
-
- showTemplateInfo(template)"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{
- getTemplateTitle(template, template.sourceModule || 'default')
- }}
-
-
- {{
- getTemplateDescription(
- template,
- template.sourceModule || 'default'
- )
- }}
-
-
-
- {{ tag }}
-
-
-
-
-
-
+
(() => {
@@ -336,14 +248,35 @@ const sortOptions = computed(() => [
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' }
])
-// Methods
-const loadTemplate = async (template: any) => {
- await loadWorkflowTemplate(template.name, template.sourceModule || 'default')
-}
+// Additional computed properties for TemplateWorkflowView
+const selectedCategoryTitle = computed(() => {
+ if (!selectedNavItem.value)
+ return t('templateWorkflows.title', 'Workflow Templates')
+
+ const navItem = navItems.value.find((item) => {
+ if ('id' in item) {
+ return item.id === selectedNavItem.value
+ }
+ return false
+ })
-const showTemplateInfo = (template: any) => {
- // TODO: Show template info modal
- console.log('Show template info for:', template)
+ if (navItem && 'title' in navItem) {
+ return navItem.title
+ }
+
+ return t('templateWorkflows.title', 'Workflow Templates')
+})
+
+const loadingTemplate = ref
(null)
+
+// Methods
+const onLoadWorkflow = async (templateName: string) => {
+ loadingTemplate.value = templateName
+ try {
+ await loadWorkflowTemplate(templateName, 'default')
+ } finally {
+ loadingTemplate.value = null
+ }
}
// Initialize
diff --git a/src/composables/useTemplateWorkflows.ts b/src/composables/useTemplateWorkflows.ts
index f10237a7de..ce4a3e8a2b 100644
--- a/src/composables/useTemplateWorkflows.ts
+++ b/src/composables/useTemplateWorkflows.ts
@@ -60,7 +60,7 @@ export function useTemplateWorkflows() {
const getTemplateThumbnailUrl = (
template: TemplateInfo,
sourceModule: string,
- index = ''
+ index = '1'
) => {
const basePath =
sourceModule === 'default'
From ce30ea3417a98fc05ed4c847dabb823f5fc3b88d Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 19:38:53 +0100
Subject: [PATCH 04/79] feat: enhance MultiSelect and SearchBox components with
improved filtering and search functionality
---
.../widget/WorkflowTemplateSelector.vue | 164 +++++++++++++-----
src/components/input/MultiSelect.vue | 42 ++++-
src/components/input/SearchBox.vue | 9 +-
3 files changed, 169 insertions(+), 46 deletions(-)
diff --git a/src/components/custom/widget/WorkflowTemplateSelector.vue b/src/components/custom/widget/WorkflowTemplateSelector.vue
index 0cfac0c509..5baa85964a 100644
--- a/src/components/custom/widget/WorkflowTemplateSelector.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelector.vue
@@ -34,17 +34,54 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- {{
- $t('templateWorkflows.activeFilters', 'Filters:')
- }}
-
-
@@ -189,13 +207,19 @@ const navigationFilteredTemplates = computed(() => {
const {
searchQuery,
selectedModels,
+ selectedUseCases,
+ selectedLicenses,
sortBy,
filteredTemplates,
availableModels,
+ availableUseCases,
+ availableLicenses,
filteredCount,
totalCount,
- resetFilters,
- removeModelFilter
+ resetFilters
+ // removeModelFilter,
+ // removeUseCaseFilter,
+ // removeLicenseFilter
} = useTemplateFiltering(navigationFilteredTemplates)
// Convert between string array and object array for MultiSelect component
@@ -208,13 +232,37 @@ const selectedModelObjects = computed({
}
})
+const selectedUseCaseObjects = computed({
+ get() {
+ return selectedUseCases.value.map((useCase) => ({
+ name: useCase,
+ value: useCase
+ }))
+ },
+ set(value: { name: string; value: string }[]) {
+ selectedUseCases.value = value.map((item) => item.value)
+ }
+})
+
+const selectedLicenseObjects = computed({
+ get() {
+ return selectedLicenses.value.map((license) => ({
+ name: license,
+ value: license
+ }))
+ },
+ set(value: { name: string; value: string }[]) {
+ selectedLicenses.value = value.map((item) => item.value)
+ }
+})
+
// Loading state
const isLoading = ref(true)
// Navigation
const selectedNavItem = ref
('all')
-// Model filter options
+// Filter options
const modelOptions = computed(() =>
availableModels.value.map((model) => ({
name: model,
@@ -222,7 +270,21 @@ const modelOptions = computed(() =>
}))
)
-// Model filter label
+const useCaseOptions = computed(() =>
+ availableUseCases.value.map((useCase) => ({
+ name: useCase,
+ value: useCase
+ }))
+)
+
+const licenseOptions = computed(() =>
+ availableLicenses.value.map((license) => ({
+ name: license,
+ value: license
+ }))
+)
+
+// Filter labels
const modelFilterLabel = computed(() => {
if (selectedModelObjects.value.length === 0) {
return t('templateWorkflows.modelFilter', 'Model Filter')
@@ -235,17 +297,41 @@ const modelFilterLabel = computed(() => {
}
})
+const useCaseFilterLabel = computed(() => {
+ if (selectedUseCaseObjects.value.length === 0) {
+ return t('templateWorkflows.useCaseFilter', 'Use Case')
+ } else if (selectedUseCaseObjects.value.length === 1) {
+ return selectedUseCaseObjects.value[0].name
+ } else {
+ return t('templateWorkflows.useCasesSelected', {
+ count: selectedUseCaseObjects.value.length
+ })
+ }
+})
+
+const licenseFilterLabel = computed(() => {
+ if (selectedLicenseObjects.value.length === 0) {
+ return t('templateWorkflows.licenseFilter', 'License')
+ } else if (selectedLicenseObjects.value.length === 1) {
+ return selectedLicenseObjects.value[0].name
+ } else {
+ return t('templateWorkflows.licensesSelected', {
+ count: selectedLicenseObjects.value.length
+ })
+ }
+})
+
// Sort options
const sortOptions = computed(() => [
- {
- name: t('templateWorkflows.sort.recommended', 'Recommended'),
- value: 'recommended'
- },
{
name: t('templateWorkflows.sort.alphabetical', 'A → Z'),
value: 'alphabetical'
},
- { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' }
+ { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
+ {
+ name: t('templateWorkflows.sort.default', 'Default'),
+ value: 'default'
+ }
])
// Additional computed properties for TemplateWorkflowView
diff --git a/src/components/input/MultiSelect.vue b/src/components/input/MultiSelect.vue
index d1d02a4f16..9f31099f49 100644
--- a/src/components/input/MultiSelect.vue
+++ b/src/components/input/MultiSelect.vue
@@ -2,7 +2,7 @@
- {{ slotProps.option.name }}
+ {{
+ slotProps.option.name
+ }}
@@ -88,6 +90,7 @@
+
\ No newline at end of file
From 989e4e50d33afcc0a7fd670482c70b169ba4fdd7 Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 19:39:37 +0100
Subject: [PATCH 05/79] feat: remove TemplateSearchBar component and enhance
template filtering options with use cases and licenses
---
.../templates/TemplateSearchBar.vue | 64 --------------
.../templates/TemplateWorkflowView.vue | 25 ------
src/composables/useTemplateFiltering.ts | 85 +++++++++++++++++--
src/locales/en/main.json | 2 +
src/types/workflowTemplateTypes.ts | 2 +
5 files changed, 82 insertions(+), 96 deletions(-)
delete mode 100644 src/components/templates/TemplateSearchBar.vue
diff --git a/src/components/templates/TemplateSearchBar.vue b/src/components/templates/TemplateSearchBar.vue
deleted file mode 100644
index 17b564b126..0000000000
--- a/src/components/templates/TemplateSearchBar.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
- {{ $t('g.resultsCount', { count: filteredCount }) }}
-
-
-
-
-
-
-
diff --git a/src/components/templates/TemplateWorkflowView.vue b/src/components/templates/TemplateWorkflowView.vue
index 8a866cdd17..c364a9dfa0 100644
--- a/src/components/templates/TemplateWorkflowView.vue
+++ b/src/components/templates/TemplateWorkflowView.vue
@@ -7,28 +7,6 @@
pt:root="h-full grid grid-rows-[auto_1fr_auto]"
pt:content="p-2 overflow-auto"
>
-
-
-
-
{{ title }}
-
-
-
-
-
-
-
reset()"
- />
-
-
-
import { useLocalStorage } from '@vueuse/core'
import DataView from 'primevue/dataview'
-import SelectButton from 'primevue/selectbutton'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
-
-import TemplateSearchBar from '@/components/templates/TemplateSearchBar.vue'
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
import TemplateWorkflowCardSkeleton from '@/components/templates/TemplateWorkflowCardSkeleton.vue'
import TemplateWorkflowList from '@/components/templates/TemplateWorkflowList.vue'
diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts
index c9f2bc1a6b..240407b003 100644
--- a/src/composables/useTemplateFiltering.ts
+++ b/src/composables/useTemplateFiltering.ts
@@ -6,7 +6,9 @@ import type { TemplateInfo } from '@/types/workflowTemplateTypes'
export interface TemplateFilterOptions {
searchQuery?: string
selectedModels?: string[]
- sortBy?: 'recommended' | 'alphabetical' | 'newest'
+ selectedUseCases?: string[] // Now represents selected tags
+ selectedLicenses?: string[]
+ sortBy?: 'default' | 'alphabetical' | 'newest'
}
export function useTemplateFiltering(
@@ -14,7 +16,9 @@ export function useTemplateFiltering(
) {
const searchQuery = ref('')
const selectedModels = ref([])
- const sortBy = ref<'recommended' | 'alphabetical' | 'newest'>('recommended')
+ const selectedUseCases = ref([])
+ const selectedLicenses = ref([])
+ const sortBy = ref<'default' | 'alphabetical' | 'newest'>('default')
const templatesArray = computed(() => {
const templateData = 'value' in templates ? templates.value : templates
@@ -47,6 +51,20 @@ export function useTemplateFiltering(
return Array.from(modelSet).sort()
})
+ const availableUseCases = computed(() => {
+ const tagSet = new Set()
+ templatesArray.value.forEach((template) => {
+ if (template.tags && Array.isArray(template.tags)) {
+ template.tags.forEach((tag) => tagSet.add(tag))
+ }
+ })
+ return Array.from(tagSet).sort()
+ })
+
+ const availableLicenses = computed(() => {
+ return ['Open Source', 'Closed Source (API Nodes)']
+ })
+
const filteredBySearch = computed(() => {
if (!searchQuery.value.trim()) {
return templatesArray.value
@@ -71,8 +89,45 @@ export function useTemplateFiltering(
})
})
+ const filteredByUseCases = computed(() => {
+ if (selectedUseCases.value.length === 0) {
+ return filteredByModels.value
+ }
+
+ return filteredByModels.value.filter((template) => {
+ if (!template.tags || !Array.isArray(template.tags)) {
+ return false
+ }
+ return selectedUseCases.value.some((selectedTag) =>
+ template.tags?.includes(selectedTag)
+ )
+ })
+ })
+
+ const filteredByLicenses = computed(() => {
+ if (selectedLicenses.value.length === 0) {
+ return filteredByUseCases.value
+ }
+
+ return filteredByUseCases.value.filter((template) => {
+ // Check if template has API in its tags or name (indicating it's a closed source API node)
+ const isApiTemplate =
+ template.tags?.includes('API') ||
+ template.name?.toLowerCase().includes('api_')
+
+ return selectedLicenses.value.some((selectedLicense) => {
+ if (selectedLicense === 'Closed Source (API Nodes)') {
+ return isApiTemplate
+ } else if (selectedLicense === 'Open Source') {
+ return !isApiTemplate
+ }
+ return false
+ })
+ })
+ })
+
const sortedTemplates = computed(() => {
- const templates = [...filteredByModels.value]
+ const templates = [...filteredByLicenses.value]
switch (sortBy.value) {
case 'alphabetical':
@@ -87,9 +142,9 @@ export function useTemplateFiltering(
const dateB = new Date(b.date || '1970-01-01')
return dateB.getTime() - dateA.getTime()
})
- case 'recommended':
+ case 'default':
default:
- // Keep original order (recommended order)
+ // Keep original order (default order)
return templates
}
})
@@ -99,13 +154,23 @@ export function useTemplateFiltering(
const resetFilters = () => {
searchQuery.value = ''
selectedModels.value = []
- sortBy.value = 'recommended'
+ selectedUseCases.value = []
+ selectedLicenses.value = []
+ sortBy.value = 'default'
}
const removeModelFilter = (model: string) => {
selectedModels.value = selectedModels.value.filter((m) => m !== model)
}
+ const removeUseCaseFilter = (tag: string) => {
+ selectedUseCases.value = selectedUseCases.value.filter((t) => t !== tag)
+ }
+
+ const removeLicenseFilter = (license: string) => {
+ selectedLicenses.value = selectedLicenses.value.filter((l) => l !== license)
+ }
+
const filteredCount = computed(() => filteredTemplates.value.length)
const totalCount = computed(() => templatesArray.value.length)
@@ -113,16 +178,22 @@ export function useTemplateFiltering(
// State
searchQuery,
selectedModels,
+ selectedUseCases,
+ selectedLicenses,
sortBy,
// Computed
filteredTemplates,
availableModels,
+ availableUseCases,
+ availableLicenses,
filteredCount,
totalCount,
// Methods
resetFilters,
- removeModelFilter
+ removeModelFilter,
+ removeUseCaseFilter,
+ removeLicenseFilter
}
}
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index c7745520a7..ea41a662fb 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -895,6 +895,8 @@
"noResultsHint": "Try adjusting your search or filters",
"modelFilter": "Model Filter",
"modelsSelected": "{count} Models",
+ "useCasesSelected": "{count} Use Cases",
+ "licensesSelected": "{count} Licenses",
"resultsCount": "Showing {count} of {total} templates",
"sort": {
"recommended": "Recommended",
diff --git a/src/types/workflowTemplateTypes.ts b/src/types/workflowTemplateTypes.ts
index 1f62102fbb..0a8d5efa90 100644
--- a/src/types/workflowTemplateTypes.ts
+++ b/src/types/workflowTemplateTypes.ts
@@ -15,6 +15,8 @@ export interface TemplateInfo {
tags?: string[]
models?: string[]
date?: string
+ useCase?: string
+ license?: string
}
export interface WorkflowTemplates {
From 18482ac385b059e17f59fa4d19e28f6c0899bbf3 Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 21:10:39 +0100
Subject: [PATCH 06/79] feat: enhance navigation and template filtering with
icon support and new category mappings
---
.../widget/WorkflowTemplateSelector.vue | 7 +-
src/components/widget/nav/NavItem.vue | 71 ++++++++++++++++--
src/components/widget/panel/LeftSidePanel.vue | 2 +
src/stores/workflowTemplatesStore.ts | 72 ++++++++++++++++---
src/types/navTypes.ts | 2 +
5 files changed, 130 insertions(+), 24 deletions(-)
diff --git a/src/components/custom/widget/WorkflowTemplateSelector.vue b/src/components/custom/widget/WorkflowTemplateSelector.vue
index 5baa85964a..925f5ef2b3 100644
--- a/src/components/custom/widget/WorkflowTemplateSelector.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelector.vue
@@ -9,7 +9,7 @@
{{
- $t('templateWorkflows.categories', 'Categories')
+ $t('sideToolbar.templates', 'Templates')
}}
@@ -146,11 +146,6 @@
-
-
-
-
-
diff --git a/src/components/widget/nav/NavItem.vue b/src/components/widget/nav/NavItem.vue
index 586d16569f..54d4647a8b 100644
--- a/src/components/widget/nav/NavItem.vue
+++ b/src/components/widget/nav/NavItem.vue
@@ -9,7 +9,7 @@
role="button"
@click="onClick"
>
-
+
@@ -17,13 +17,70 @@
diff --git a/src/components/widget/panel/LeftSidePanel.vue b/src/components/widget/panel/LeftSidePanel.vue
index 425d371323..bcc84ffde0 100644
--- a/src/components/widget/panel/LeftSidePanel.vue
+++ b/src/components/widget/panel/LeftSidePanel.vue
@@ -14,6 +14,7 @@
@@ -22,6 +23,7 @@
diff --git a/src/stores/workflowTemplatesStore.ts b/src/stores/workflowTemplatesStore.ts
index f190d39522..9c85e84817 100644
--- a/src/stores/workflowTemplatesStore.ts
+++ b/src/stores/workflowTemplatesStore.ts
@@ -10,6 +10,7 @@ import type {
TemplateInfo,
WorkflowTemplates
} from '@/types/workflowTemplateTypes'
+import { getCategoryIcon } from '@/utils/categoryIcons'
import { normalizeI18nKey } from '@/utils/formatUtil'
// Enhanced template interface for easier filtering
@@ -302,6 +303,15 @@ export const useWorkflowTemplatesStore = defineStore(
(t) => t.sourceModule !== 'default'
)
+ case 'lora-training':
+ return enhancedTemplates.value.filter(
+ (t) =>
+ t.tags?.includes('LoRA') ||
+ t.tags?.includes('Training') ||
+ t.name?.toLowerCase().includes('lora') ||
+ t.title?.toLowerCase().includes('lora')
+ )
+
case 'performance-small':
return enhancedTemplates.value.filter((t) => t.isPerformance)
@@ -347,11 +357,19 @@ export const useWorkflowTemplatesStore = defineStore(
const macCompatibleCounts = enhancedTemplates.value.filter(
(t) => t.isMacCompatible
).length
+ const loraTrainingCounts = enhancedTemplates.value.filter(
+ (t) =>
+ t.tags?.includes('LoRA') ||
+ t.tags?.includes('Training') ||
+ t.name?.toLowerCase().includes('lora') ||
+ t.title?.toLowerCase().includes('lora')
+ ).length
// All Templates - as a simple selector
items.push({
id: 'all',
- label: st('templateWorkflows.category.All', 'All Templates')
+ label: st('templateWorkflows.category.All', 'All Templates'),
+ icon: getCategoryIcon('all')
})
// Getting Started - as a simple selector
@@ -361,7 +379,8 @@ export const useWorkflowTemplatesStore = defineStore(
label: st(
'templateWorkflows.category.GettingStarted',
'Getting Started'
- )
+ ),
+ icon: getCategoryIcon('getting-started')
})
}
@@ -377,28 +396,32 @@ export const useWorkflowTemplatesStore = defineStore(
if (imageCounts > 0) {
generationTypeItems.push({
id: 'generation-image',
- label: st('templateWorkflows.category.Image', 'Image')
+ label: st('templateWorkflows.category.Image', 'Image'),
+ icon: getCategoryIcon('generation-image')
})
}
if (videoCounts > 0) {
generationTypeItems.push({
id: 'generation-video',
- label: st('templateWorkflows.category.Video', 'Video')
+ label: st('templateWorkflows.category.Video', 'Video'),
+ icon: getCategoryIcon('generation-video')
})
}
if (threeDCounts > 0) {
generationTypeItems.push({
id: 'generation-3d',
- label: st('templateWorkflows.category.3DModels', '3D Models')
+ label: st('templateWorkflows.category.3DModels', '3D Models'),
+ icon: getCategoryIcon('generation-3d')
})
}
if (audioCounts > 0) {
generationTypeItems.push({
id: 'generation-audio',
- label: st('templateWorkflows.category.Audio', 'Audio')
+ label: st('templateWorkflows.category.Audio', 'Audio'),
+ icon: getCategoryIcon('generation-audio')
})
}
@@ -414,11 +437,15 @@ export const useWorkflowTemplatesStore = defineStore(
// Closed Models (API nodes) - as a group
if (apiCounts > 0) {
items.push({
- title: st('templateWorkflows.category.ClosedModels', 'Closed Models'),
+ title: st(
+ 'templateWorkflows.category.ClosedSourceModels',
+ 'Closed Source Models'
+ ),
items: [
{
id: 'api-nodes',
- label: st('templateWorkflows.category.APINodes', 'API nodes')
+ label: st('templateWorkflows.category.APINodes', 'API nodes'),
+ icon: getCategoryIcon('api-nodes')
}
]
})
@@ -428,7 +455,28 @@ export const useWorkflowTemplatesStore = defineStore(
if (extensionCounts > 0) {
items.push({
id: 'extensions',
- label: st('templateWorkflows.category.Extensions', 'Extensions')
+ label: st('templateWorkflows.category.Extensions', 'Extensions'),
+ icon: getCategoryIcon('extensions')
+ })
+ }
+
+ // Model Training - as a group
+ if (loraTrainingCounts > 0) {
+ items.push({
+ title: st(
+ 'templateWorkflows.category.ModelTraining',
+ 'Model Training'
+ ),
+ items: [
+ {
+ id: 'lora-training',
+ label: st(
+ 'templateWorkflows.category.LoRATraining',
+ 'LoRA Training'
+ ),
+ icon: getCategoryIcon('lora-training')
+ }
+ ]
})
}
@@ -437,7 +485,8 @@ export const useWorkflowTemplatesStore = defineStore(
const performanceItems: NavItemData[] = [
{
id: 'performance-small',
- label: st('templateWorkflows.category.SmallModels', 'Small Models')
+ label: st('templateWorkflows.category.SmallModels', 'Small Models'),
+ icon: getCategoryIcon('small-models')
}
]
@@ -448,7 +497,8 @@ export const useWorkflowTemplatesStore = defineStore(
label: st(
'templateWorkflows.category.RunsOnMac',
'Runs on Mac (Silicon)'
- )
+ ),
+ icon: getCategoryIcon('runs-on-mac')
})
}
diff --git a/src/types/navTypes.ts b/src/types/navTypes.ts
index 785faedb49..ec199b59cd 100644
--- a/src/types/navTypes.ts
+++ b/src/types/navTypes.ts
@@ -1,9 +1,11 @@
export interface NavItemData {
id: string
label: string
+ icon?: string
}
export interface NavGroupData {
title: string
items: NavItemData[]
+ icon?: string
}
From 9ec2f692824d3b5cc25fbfba0162990d5850b95d Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 21:11:19 +0100
Subject: [PATCH 07/79] feat: add category icon mapping functions for IDs and
titles
---
src/utils/categoryIcons.ts | 70 ++++++++++++++++++++++++++++++++++++++
1 file changed, 70 insertions(+)
create mode 100644 src/utils/categoryIcons.ts
diff --git a/src/utils/categoryIcons.ts b/src/utils/categoryIcons.ts
new file mode 100644
index 0000000000..6d81a403bd
--- /dev/null
+++ b/src/utils/categoryIcons.ts
@@ -0,0 +1,70 @@
+/**
+ * Maps category IDs to their corresponding Lucide icon names
+ */
+export const getCategoryIcon = (categoryId: string): string => {
+ const iconMap: Record = {
+ // Main categories
+ all: 'list',
+ 'getting-started': 'graduation-cap',
+
+ // Generation types
+ 'generation-image': 'image',
+ image: 'image',
+ 'generation-video': 'film',
+ video: 'film',
+ 'generation-3d': 'box',
+ '3d': 'box',
+ 'generation-audio': 'volume-2',
+ audio: 'volume-2',
+
+ // API and models
+ 'api-nodes': 'hand-coins',
+ 'closed-models': 'hand-coins',
+
+ // LLMs and AI
+ llm: 'message-square-text',
+ llms: 'message-square-text',
+
+ // Performance and hardware
+ 'small-models': 'zap',
+ performance: 'zap',
+ 'mac-compatible': 'command',
+ 'runs-on-mac': 'command',
+
+ // Training
+ 'lora-training': 'dumbbell',
+ training: 'dumbbell',
+
+ // Extensions and tools
+ extensions: 'puzzle',
+ tools: 'wrench',
+
+ // Fallbacks for common patterns
+ upscaling: 'maximize-2',
+ controlnet: 'sliders-horizontal',
+ 'area-composition': 'layout-grid'
+ }
+
+ // Return mapped icon or fallback to folder
+ return iconMap[categoryId.toLowerCase()] || 'folder'
+}
+
+/**
+ * Maps category titles to their corresponding Lucide icon names
+ */
+export const getCategoryIconByTitle = (title: string): string => {
+ const titleMap: Record = {
+ 'Getting Started': 'graduation-cap',
+ 'Generation Type': 'sparkles',
+ 'Closed Source Models': 'hand-coins',
+ 'API Nodes': 'hand-coins',
+ 'Small Models': 'zap',
+ Performance: 'zap',
+ 'Mac Compatible': 'command',
+ 'LoRA Training': 'dumbbell',
+ Extensions: 'puzzle',
+ 'Tools & Building': 'wrench'
+ }
+
+ return titleMap[title] || 'folder'
+}
From 8fb225f1f65badafdca2e1ef2564cb284b20d92d Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Wed, 20 Aug 2025 22:31:17 +0100
Subject: [PATCH 08/79] feat: add new sorting options for template filtering by
VRAM utilization and model size
---
.../custom/widget/WorkflowTemplateSelector.vue | 16 ++++++++++++----
src/composables/useTemplateFiltering.ts | 12 ++++++++++--
2 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/src/components/custom/widget/WorkflowTemplateSelector.vue b/src/components/custom/widget/WorkflowTemplateSelector.vue
index 925f5ef2b3..8102b78d7a 100644
--- a/src/components/custom/widget/WorkflowTemplateSelector.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelector.vue
@@ -319,13 +319,21 @@ const licenseFilterLabel = computed(() => {
// Sort options
const sortOptions = computed(() => [
{
- name: t('templateWorkflows.sort.alphabetical', 'A → Z'),
- value: 'alphabetical'
+ name: t('templateWorkflows.sort.default', 'Default'),
+ value: 'default'
},
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
{
- name: t('templateWorkflows.sort.default', 'Default'),
- value: 'default'
+ name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Utilization (Low to High)'),
+ value: 'vram-low-to-high'
+ },
+ {
+ name: t('templateWorkflows.sort.modelSizeLowToHigh', 'Model Size (Low to High)'),
+ value: 'model-size-low-to-high'
+ },
+ {
+ name: t('templateWorkflows.sort.alphabetical', 'Alphabetical (A-Z)'),
+ value: 'alphabetical'
}
])
diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts
index 240407b003..03c18929e2 100644
--- a/src/composables/useTemplateFiltering.ts
+++ b/src/composables/useTemplateFiltering.ts
@@ -8,7 +8,7 @@ export interface TemplateFilterOptions {
selectedModels?: string[]
selectedUseCases?: string[] // Now represents selected tags
selectedLicenses?: string[]
- sortBy?: 'default' | 'alphabetical' | 'newest'
+ sortBy?: 'default' | 'alphabetical' | 'newest' | 'vram-low-to-high' | 'model-size-low-to-high'
}
export function useTemplateFiltering(
@@ -18,7 +18,7 @@ export function useTemplateFiltering(
const selectedModels = ref([])
const selectedUseCases = ref([])
const selectedLicenses = ref([])
- const sortBy = ref<'default' | 'alphabetical' | 'newest'>('default')
+ const sortBy = ref<'default' | 'alphabetical' | 'newest' | 'vram-low-to-high' | 'model-size-low-to-high'>('default')
const templatesArray = computed(() => {
const templateData = 'value' in templates ? templates.value : templates
@@ -142,6 +142,14 @@ export function useTemplateFiltering(
const dateB = new Date(b.date || '1970-01-01')
return dateB.getTime() - dateA.getTime()
})
+ case 'vram-low-to-high':
+ // TODO: Implement VRAM sorting when VRAM data is available
+ // For now, keep original order
+ return templates
+ case 'model-size-low-to-high':
+ // TODO: Implement model size sorting when model size data is available
+ // For now, keep original order
+ return templates
case 'default':
default:
// Keep original order (default order)
From c6d6de74d6da606be238f7cb6bf189174db158fa Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Thu, 21 Aug 2025 02:43:10 +0100
Subject: [PATCH 09/79] [skip ci]
---
src/composables/useWorkflowTemplateSelectorDialog.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/composables/useWorkflowTemplateSelectorDialog.ts b/src/composables/useWorkflowTemplateSelectorDialog.ts
index bcb944a46e..a99217659e 100644
--- a/src/composables/useWorkflowTemplateSelectorDialog.ts
+++ b/src/composables/useWorkflowTemplateSelectorDialog.ts
@@ -2,7 +2,7 @@ import WorkflowTemplateSelector from '@/components/custom/widget/WorkflowTemplat
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
-const DIALOG_KEY = 'global-workflow-template-selector'
+const DIALOG_KEY = 'global-workflow-template-selector';
export const useWorkflowTemplateSelectorDialog = () => {
const dialogService = useDialogService()
From 5d88200aaec2ab6b655cb873fe084b8f3450e559 Mon Sep 17 00:00:00 2001
From: Johnpaul
Date: Thu, 28 Aug 2025 01:00:00 +0100
Subject: [PATCH 10/79] fix layout overflow issues on bigger screens
---
.../widget/WorkflowTemplateSelector.vue | 28 +++++++--
src/components/input/SearchBox.vue | 2 +-
.../templates/TemplateWorkflowCard.vue | 26 +++++---
.../TemplateWorkflowCardSkeleton.vue | 8 +--
.../templates/TemplateWorkflowView.vue | 6 +-
.../templates/TemplateWorkflowsContent.vue | 4 +-
.../templates/thumbnails/BaseThumbnail.vue | 4 +-
.../widget/layout/BaseWidgetLayout.vue | 37 ++++++++++--
src/components/widget/nav/NavItem.vue | 60 +++++++++----------
src/composables/useTemplateFiltering.ts | 15 ++++-
.../useWorkflowTemplateSelectorDialog.ts | 2 +-
src/services/dialogService.ts | 11 +++-
12 files changed, 140 insertions(+), 63 deletions(-)
diff --git a/src/components/custom/widget/WorkflowTemplateSelector.vue b/src/components/custom/widget/WorkflowTemplateSelector.vue
index 8102b78d7a..da98c973ac 100644
--- a/src/components/custom/widget/WorkflowTemplateSelector.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelector.vue
@@ -1,6 +1,7 @@
@@ -145,7 +146,6 @@
}}
-
@@ -160,7 +160,6 @@ import SingleSelect from '@/components/input/SingleSelect.vue'
import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
import BaseWidgetLayout from '@/components/widget/layout/BaseWidgetLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
-import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
@@ -324,11 +323,17 @@ const sortOptions = computed(() => [
},
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
{
- name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Utilization (Low to High)'),
+ name: t(
+ 'templateWorkflows.sort.vramLowToHigh',
+ 'VRAM Utilization (Low to High)'
+ ),
value: 'vram-low-to-high'
},
{
- name: t('templateWorkflows.sort.modelSizeLowToHigh', 'Model Size (Low to High)'),
+ name: t(
+ 'templateWorkflows.sort.modelSizeLowToHigh',
+ 'Model Size (Low to High)'
+ ),
value: 'model-size-low-to-high'
},
{
@@ -375,3 +380,18 @@ onMounted(async () => {
isLoading.value = false
})
+
+
diff --git a/src/components/input/SearchBox.vue b/src/components/input/SearchBox.vue
index 7dfe004aba..e8275b4d01 100644
--- a/src/components/input/SearchBox.vue
+++ b/src/components/input/SearchBox.vue
@@ -33,4 +33,4 @@ const wrapperStyle = computed(() => {
const iconColorStyle = computed(() => {
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
})
-
\ No newline at end of file
+
diff --git a/src/components/templates/TemplateWorkflowCard.vue b/src/components/templates/TemplateWorkflowCard.vue
index 4860b9c778..af0c770d47 100644
--- a/src/components/templates/TemplateWorkflowCard.vue
+++ b/src/components/templates/TemplateWorkflowCard.vue
@@ -2,15 +2,15 @@
-
-
+
+
-
-
-
+
+
+
{{ title }}
-
+
{{ description }}
+
+
+ {{ tag }}
+
+
diff --git a/src/components/templates/TemplateWorkflowCardSkeleton.vue b/src/components/templates/TemplateWorkflowCardSkeleton.vue
index 00bf738398..856c6e5eac 100644
--- a/src/components/templates/TemplateWorkflowCardSkeleton.vue
+++ b/src/components/templates/TemplateWorkflowCardSkeleton.vue
@@ -1,14 +1,14 @@
-
-
-
+
diff --git a/src/components/templates/TemplateWorkflowView.vue b/src/components/templates/TemplateWorkflowView.vue
index c364a9dfa0..f3db7f2ee7 100644
--- a/src/components/templates/TemplateWorkflowView.vue
+++ b/src/components/templates/TemplateWorkflowView.vue
@@ -57,6 +57,7 @@ import { useLocalStorage } from '@vueuse/core'
import DataView from 'primevue/dataview'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
+
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
import TemplateWorkflowCardSkeleton from '@/components/templates/TemplateWorkflowCardSkeleton.vue'
import TemplateWorkflowList from '@/components/templates/TemplateWorkflowList.vue'
@@ -67,7 +68,7 @@ import type { TemplateInfo } from '@/types/workflowTemplateTypes'
const { t } = useI18n()
-const { title, sourceModule, categoryTitle, loading, templates } = defineProps<{
+const { sourceModule, categoryTitle, loading, templates } = defineProps<{
title: string
sourceModule: string
categoryTitle: string
@@ -85,8 +86,7 @@ const loadTrigger = ref
(null)
const templatesRef = computed(() => templates || [])
-const { searchQuery, filteredTemplates, filteredCount } =
- useTemplateFiltering(templatesRef)
+const { searchQuery, filteredTemplates } = useTemplateFiltering(templatesRef)
// When searching, show all results immediately without pagination
// When not searching, use lazy pagination
diff --git a/src/components/templates/TemplateWorkflowsContent.vue b/src/components/templates/TemplateWorkflowsContent.vue
index 26e63a4bca..54bd0c9866 100644
--- a/src/components/templates/TemplateWorkflowsContent.vue
+++ b/src/components/templates/TemplateWorkflowsContent.vue
@@ -1,6 +1,6 @@