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 @@ + + + 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 @@

-
- - - - -
+
(() => { @@ -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 @@ - - - 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" > - - @@ -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 @@ @@ -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 @@ 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 @@