From 5447dfe1675874dd4f65e76140c1d6c7a1885e97 Mon Sep 17 00:00:00 2001 From: Yourz Date: Thu, 1 Jan 2026 23:22:25 +0800 Subject: [PATCH 1/6] feat: add dynamic Fuse.js options loading for template filtering --- .../widget/WorkflowTemplateSelectorDialog.vue | 6 +- src/composables/useTemplateFiltering.test.ts | 122 ++++++++++++++++++ src/composables/useTemplateFiltering.ts | 46 ++++--- .../repositories/workflowTemplatesStore.ts | 20 --- src/scripts/api.ts | 17 +++ 5 files changed, 172 insertions(+), 39 deletions(-) diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue index 7c76e971d8..08305078cd 100644 --- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -563,7 +563,8 @@ const { availableRunsOn, filteredCount, totalCount, - resetFilters + resetFilters, + loadFuseOptions } = useTemplateFiltering(navigationFilteredTemplates) /** @@ -818,7 +819,8 @@ const { isLoading } = useAsyncState( // Run all operations in parallel for better performance await Promise.all([ loadTemplates(), - workflowTemplatesStore.loadWorkflowTemplates() + workflowTemplatesStore.loadWorkflowTemplates(), + loadFuseOptions() ]) return true }, diff --git a/src/composables/useTemplateFiltering.test.ts b/src/composables/useTemplateFiltering.test.ts index 5f30e5ec1b..ef4332236b 100644 --- a/src/composables/useTemplateFiltering.test.ts +++ b/src/composables/useTemplateFiltering.test.ts @@ -42,6 +42,13 @@ vi.mock('@/platform/telemetry', () => ({ })) })) +const mockGetFuseOptions = vi.hoisted(() => vi.fn()) +vi.mock('@/scripts/api', () => ({ + api: { + getFuseOptions: mockGetFuseOptions + } +})) + const { useTemplateFiltering } = await import('@/composables/useTemplateFiltering') @@ -49,6 +56,7 @@ describe('useTemplateFiltering', () => { beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() + mockGetFuseOptions.mockResolvedValue(null) }) afterEach(() => { @@ -272,4 +280,118 @@ describe('useTemplateFiltering', () => { 'beta-pro' ]) }) + + describe('loadFuseOptions', () => { + it('updates fuseOptions when getFuseOptions returns valid options', async () => { + const templates = ref([ + { + name: 'test-template', + description: 'Test template', + mediaType: 'image', + mediaSubtype: 'png' + } + ]) + + const customFuseOptions = { + keys: [ + { name: 'name', weight: 0.5 }, + { name: 'description', weight: 0.5 } + ], + threshold: 0.4, + includeScore: true + } + + mockGetFuseOptions.mockResolvedValueOnce(customFuseOptions) + + const { loadFuseOptions, filteredTemplates } = + useTemplateFiltering(templates) + + await loadFuseOptions() + + expect(mockGetFuseOptions).toHaveBeenCalledTimes(1) + expect(filteredTemplates.value).toBeDefined() + }) + + it('does not update fuseOptions when getFuseOptions returns null', async () => { + const templates = ref([ + { + name: 'test-template', + description: 'Test template', + mediaType: 'image', + mediaSubtype: 'png' + } + ]) + + mockGetFuseOptions.mockResolvedValueOnce(null) + + const { loadFuseOptions, filteredTemplates } = + useTemplateFiltering(templates) + + const initialResults = filteredTemplates.value + + await loadFuseOptions() + + expect(mockGetFuseOptions).toHaveBeenCalledTimes(1) + expect(filteredTemplates.value).toEqual(initialResults) + }) + + it('handles errors when getFuseOptions fails', async () => { + const templates = ref([ + { + name: 'test-template', + description: 'Test template', + mediaType: 'image', + mediaSubtype: 'png' + } + ]) + + mockGetFuseOptions.mockRejectedValueOnce(new Error('Network error')) + + const { loadFuseOptions, filteredTemplates } = + useTemplateFiltering(templates) + + const initialResults = filteredTemplates.value + + await expect(loadFuseOptions()).rejects.toThrow('Network error') + expect(filteredTemplates.value).toEqual(initialResults) + }) + + it('recreates Fuse instance when fuseOptions change', async () => { + const templates = ref([ + { + name: 'searchable-template', + description: 'This is a searchable template', + mediaType: 'image', + mediaSubtype: 'png' + }, + { + name: 'another-template', + description: 'Another template', + mediaType: 'image', + mediaSubtype: 'png' + } + ]) + + const { loadFuseOptions, searchQuery, filteredTemplates } = + useTemplateFiltering(templates) + + const customFuseOptions = { + keys: [{ name: 'name', weight: 1.0 }], + threshold: 0.2, + includeScore: true, + includeMatches: true + } + + mockGetFuseOptions.mockResolvedValueOnce(customFuseOptions) + + await loadFuseOptions() + await nextTick() + + searchQuery.value = 'searchable' + await nextTick() + + expect(filteredTemplates.value.length).toBeGreaterThan(0) + expect(mockGetFuseOptions).toHaveBeenCalledTimes(1) + }) + }) }) diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts index fdb892e07b..903f788a64 100644 --- a/src/composables/useTemplateFiltering.ts +++ b/src/composables/useTemplateFiltering.ts @@ -1,5 +1,6 @@ import { refDebounced, watchDebounced } from '@vueuse/core' -import Fuse from 'fuse.js' +import Fuse from 'fuse.js'; +import type { IFuseOptions } from 'fuse.js'; import { computed, ref, watch } from 'vue' import type { Ref } from 'vue' @@ -8,6 +9,21 @@ import { useTelemetry } from '@/platform/telemetry' import type { TemplateInfo } from '@/platform/workflow/templates/types/template' import { useTemplateRankingStore } from '@/stores/templateRankingStore' import { debounce } from 'es-toolkit/compat' +import { api } from '@/scripts/api' + +// Fuse.js configuration for fuzzy search +const defaultFuseOptions = { + keys: [ + { name: 'name', weight: 0.3 }, + { name: 'title', weight: 0.3 }, + { name: 'description', weight: 0.1 }, + { name: 'tags', weight: 0.2 }, + { name: 'models', weight: 0.3 } + ], + threshold: 0.33, + includeScore: true, + includeMatches: true +} export function useTemplateFiltering( templates: Ref | TemplateInfo[] @@ -35,26 +51,14 @@ export function useTemplateFiltering( | 'model-size-low-to-high' >(settingStore.get('Comfy.Templates.SortBy')) + const fuseOptions = ref>(defaultFuseOptions) + const templatesArray = computed(() => { const templateData = 'value' in templates ? templates.value : templates return Array.isArray(templateData) ? templateData : [] }) - // Fuse.js configuration for fuzzy search - const fuseOptions = { - keys: [ - { name: 'name', weight: 0.3 }, - { name: 'title', weight: 0.3 }, - { name: 'description', weight: 0.1 }, - { name: 'tags', weight: 0.2 }, - { name: 'models', weight: 0.3 } - ], - threshold: 0.33, - includeScore: true, - includeMatches: true - } - - const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions)) + const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions.value)) const availableModels = computed(() => { const modelSet = new Set() @@ -272,6 +276,13 @@ export function useTemplateFiltering( }) }, 500) + const loadFuseOptions = async () => { + const fetchedOptions = await api.getFuseOptions() + if (fetchedOptions) { + fuseOptions.value = fetchedOptions + } + } + // Watch for filter changes and track them watch( [searchQuery, selectedModels, selectedUseCases, selectedRunsOn, sortBy], @@ -344,6 +355,7 @@ export function useTemplateFiltering( resetFilters, removeModelFilter, removeUseCaseFilter, - removeRunsOnFilter + removeRunsOnFilter, + loadFuseOptions } } diff --git a/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts b/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts index 8ce653d4da..f96872f79e 100644 --- a/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts +++ b/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts @@ -1,4 +1,3 @@ -import Fuse from 'fuse.js' import { defineStore } from 'pinia' import { computed, ref, shallowRef } from 'vue' @@ -250,24 +249,6 @@ export const useWorkflowTemplatesStore = defineStore( return filteredTemplates }) - /** - * 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 new Fuse(enhancedTemplates.value, fuseOptions) - }) - /** * Filter templates by category ID using stored filter mappings */ @@ -548,7 +529,6 @@ export const useWorkflowTemplatesStore = defineStore( groupedTemplates, navGroupedTemplates, enhancedTemplates, - templateFuse, filterTemplatesByCategory, isLoaded, loadWorkflowTemplates, diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 3061f74bdb..366389729d 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -51,6 +51,7 @@ import type { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import type { AuthHeader } from '@/types/authTypes' import type { NodeExecutionId } from '@/types/nodeIdentification' import { fetchHistory } from '@/platform/remote/comfyui/history' +import type { IFuseOptions } from 'fuse.js' interface QueuePromptRequestBody { client_id: string @@ -1269,6 +1270,22 @@ export class ComfyApi extends EventTarget { } } + /** + * Gets the Fuse options from the server. + * + * @returns The Fuse options, or null if not found or invalid + */ + async getFuseOptions(): Promise | null> { + try { + const res = await axios.get(this.fileURL('/templates/fuse_options.json')) + const contentType = res.headers['content-type'] + return contentType?.includes('application/json') ? res.data : null + } catch (error) { + console.error('Error loading fuse options:', error) + return null + } + } + /** * Gets the custom nodes i18n data from the server. * From da38db7ff5687c403b1a3edf0df0b3226c480115 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 1 Jan 2026 15:24:54 +0000 Subject: [PATCH 2/6] [automated] Apply ESLint and Prettier fixes --- src/composables/useTemplateFiltering.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts index 903f788a64..440fb002d2 100644 --- a/src/composables/useTemplateFiltering.ts +++ b/src/composables/useTemplateFiltering.ts @@ -1,6 +1,6 @@ import { refDebounced, watchDebounced } from '@vueuse/core' -import Fuse from 'fuse.js'; -import type { IFuseOptions } from 'fuse.js'; +import Fuse from 'fuse.js' +import type { IFuseOptions } from 'fuse.js' import { computed, ref, watch } from 'vue' import type { Ref } from 'vue' From f6fe5cdc64e2b3cf4dd3d91a5c0ef353cf28e9e3 Mon Sep 17 00:00:00 2001 From: Yourz Date: Tue, 6 Jan 2026 00:15:59 +0800 Subject: [PATCH 3/6] fix: update for coderabbitai --- src/composables/useTemplateFiltering.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/composables/useTemplateFiltering.ts b/src/composables/useTemplateFiltering.ts index 440fb002d2..9dcdeffcc0 100644 --- a/src/composables/useTemplateFiltering.ts +++ b/src/composables/useTemplateFiltering.ts @@ -12,7 +12,7 @@ import { debounce } from 'es-toolkit/compat' import { api } from '@/scripts/api' // Fuse.js configuration for fuzzy search -const defaultFuseOptions = { +const defaultFuseOptions: IFuseOptions = { keys: [ { name: 'name', weight: 0.3 }, { name: 'title', weight: 0.3 }, @@ -51,7 +51,7 @@ export function useTemplateFiltering( | 'model-size-low-to-high' >(settingStore.get('Comfy.Templates.SortBy')) - const fuseOptions = ref>(defaultFuseOptions) + const fuseOptions = ref>(defaultFuseOptions) const templatesArray = computed(() => { const templateData = 'value' in templates ? templates.value : templates From 0deba4dd49c10b86d3f1ff943b3077503b8343a1 Mon Sep 17 00:00:00 2001 From: Yourz Date: Tue, 6 Jan 2026 19:10:09 +0800 Subject: [PATCH 4/6] fix: update for reviews --- src/composables/useTemplateFiltering.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/composables/useTemplateFiltering.test.ts b/src/composables/useTemplateFiltering.test.ts index ef4332236b..f6e617cb72 100644 --- a/src/composables/useTemplateFiltering.test.ts +++ b/src/composables/useTemplateFiltering.test.ts @@ -1,6 +1,7 @@ import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' +import type { IFuseOptions } from 'fuse.js' import type { TemplateInfo } from '@/platform/workflow/templates/types/template' @@ -292,7 +293,7 @@ describe('useTemplateFiltering', () => { } ]) - const customFuseOptions = { + const customFuseOptions: IFuseOptions = { keys: [ { name: 'name', weight: 0.5 }, { name: 'description', weight: 0.5 } From 255a621ce9bfb622cd1f814afb27a1ad2747629a Mon Sep 17 00:00:00 2001 From: Yourz Date: Wed, 7 Jan 2026 17:00:50 +0800 Subject: [PATCH 5/6] fix: update for reviews --- .../widget/WorkflowTemplateSelectorDialog.vue | 1 - src/scripts/api.ts | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue index 08305078cd..aeb98971ba 100644 --- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -816,7 +816,6 @@ const pageTitle = computed(() => { // Initialize templates loading with useAsyncState const { isLoading } = useAsyncState( async () => { - // Run all operations in parallel for better performance await Promise.all([ loadTemplates(), workflowTemplatesStore.loadWorkflowTemplates(), diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 366389729d..8da4364320 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -10,7 +10,10 @@ import type { } from '@/platform/assets/schemas/assetSchema' import { isCloud } from '@/platform/distribution/types' import { useToastStore } from '@/platform/updates/common/toastStore' -import { type WorkflowTemplates } from '@/platform/workflow/templates/types/template' +import { + type TemplateInfo, + type WorkflowTemplates +} from '@/platform/workflow/templates/types/template' import type { ComfyApiWorkflow, ComfyWorkflowJSON, @@ -1275,11 +1278,17 @@ export class ComfyApi extends EventTarget { * * @returns The Fuse options, or null if not found or invalid */ - async getFuseOptions(): Promise | null> { + async getFuseOptions(): Promise | null> { try { - const res = await axios.get(this.fileURL('/templates/fuse_options.json')) - const contentType = res.headers['content-type'] - return contentType?.includes('application/json') ? res.data : null + const res = await axios.get( + this.fileURL('/templates/fuse_options.json'), + { + headers: { + 'Content-Type': 'application/json' + } + } + ) + return res?.data ?? null } catch (error) { console.error('Error loading fuse options:', error) return null From f3e4de57b3665fbc84f964c515ba3d25ddcfc139 Mon Sep 17 00:00:00 2001 From: Yourz Date: Thu, 8 Jan 2026 10:19:14 +0800 Subject: [PATCH 6/6] fix: handle contnet type validation --- src/scripts/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 8da4364320..822773c0d6 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1288,7 +1288,8 @@ export class ComfyApi extends EventTarget { } } ) - return res?.data ?? null + const contentType = res.headers['content-type'] + return contentType?.includes('application/json') ? res.data : null } catch (error) { console.error('Error loading fuse options:', error) return null