Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 32 additions & 21 deletions src/composables/useTemplateFiltering.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import { refDebounced, watchDebounced } from '@vueuse/core'
import Fuse from 'fuse.js'
import { computed, ref, watch } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import type { Ref } from 'vue'

import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
import {
DEFAULT_TEMPLATE_FUSE_CONFIG,
TEMPLATE_FUSE_SETTINGS_KEY,
buildTemplateFuseOptions
} from '@/platform/workflow/templates/utils/templateFuseOptions'
import { TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY } from '@/platform/workflow/templates/utils/templateSearchLabInjection'
import { debounce } from 'es-toolkit/compat'

export function useTemplateFiltering(
templates: Ref<TemplateInfo[]> | TemplateInfo[]
) {
const settingStore = useSettingStore()

const searchQuery = ref('')
const injectedSearchQuery = inject<Ref<string> | null>(
TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY,
null
)
const searchQuery = injectedSearchQuery ?? ref('')
const selectedModels = ref<string[]>(
settingStore.get('Comfy.Templates.SelectedModels')
settingStore.get('Comfy.Templates.SelectedModels') ?? []
)
const selectedUseCases = ref<string[]>(
settingStore.get('Comfy.Templates.SelectedUseCases')
settingStore.get('Comfy.Templates.SelectedUseCases') ?? []
)
const selectedRunsOn = ref<string[]>(
settingStore.get('Comfy.Templates.SelectedRunsOn')
settingStore.get('Comfy.Templates.SelectedRunsOn') ?? []
)
const sortBy = ref<
| 'default'
Expand All @@ -36,21 +46,24 @@ export function useTemplateFiltering(
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.2 },
{ name: 'tags', weight: 0.1 },
{ name: 'models', weight: 0.1 }
],
threshold: 0.4,
includeScore: true,
includeMatches: true
}
const fuseConfig = computed(
() =>
settingStore.get(TEMPLATE_FUSE_SETTINGS_KEY) ??
DEFAULT_TEMPLATE_FUSE_CONFIG
)

const debouncedSearchQuery = refDebounced(searchQuery, 50)

const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions))
const fuse = computed(
() =>
new Fuse(
templatesArray.value,
buildTemplateFuseOptions<TemplateInfo>({
config: fuseConfig.value,
query: debouncedSearchQuery.value.trim().toLowerCase()
})
)
)

const availableModels = computed(() => {
const modelSet = new Set<string>()
Expand All @@ -76,8 +89,6 @@ export function useTemplateFiltering(
return ['ComfyUI', 'External or Remote API']
})

const debouncedSearchQuery = refDebounced(searchQuery, 50)

const filteredBySearch = computed(() => {
if (!debouncedSearchQuery.value.trim()) {
return templatesArray.value
Expand Down
149 changes: 148 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,153 @@
"searchPlaceholder": "Search..."
}
},
"templateSearchLab": {
"title": "Template Search Tuning Lab",
"subtitle": "Fine-tune Fuse.js options and instantly preview template results.",
"description": "Use real template data to decide how strict or fuzzy searches like \"Wan\" and \"Animate\" should feel before handing settings to engineering.",
"reset": "Reset to recommended defaults",
"configCopied": "Copied",
"configCopy": "Copy JSON",
"searchLabel": "Type a template, model, or tag",
"searchPlaceholder": "Search templates (try \"Wan\")",
"samplesLabel": "Quick queries",
"previewTitle": "Live Preview",
"previewCount": "{count} matches",
"loadingTemplates": "Loading template catalog...",
"previewEmptyState": "Start typing or tap a sample query to preview matches.",
"previewNoResults": "No templates match \"{query}\".",
"scoreLabel": "Score",
"matchLabel": "Match found in {field}",
"unknownField": "unknown field",
"matchFallback": "Enable includeMatches to view highlighted indices.",
"configHeading": "Fuse option snapshot",
"configSubheading": "Copy this block into the composable or share it with engineering.",
"additionalReadingTitle": "Helpful reading",
"additionalReadingSubtitle": "Deep dives that explain how scoring and extended syntax work.",
"previewSummaryIdle": "Waiting for a query…",
"previewSummaryEmpty": "Search ran but no templates matched.",
"previewSummaryActive": "Showing {count} of {total} templates.",
"docLinkLabel": "Docs",
"links": {
"apiDocs": "Fuse option reference",
"scoringTheory": "Scoring theory explainer",
"extendedSearch": "Extended search syntax"
},
"dialogPreviewTitle": "Live Templates Dialog",
"dialogPreviewSubtitle": "Same component designers see in production, updated in real time as you tweak settings.",
"optionCountSuffix": "settings",
"optionGroups": {
"basic": {
"title": "Basic options",
"description": "Quick toggles that control how literal or fuzzy the search feels."
},
"fuzzy": {
"title": "Fuzzy matching",
"description": "Control how tolerant we are of typos and offsets."
},
"advanced": {
"title": "Advanced",
"description": "Ranking strategies, extended syntax, and scoring heuristics."
}
},
"sortLabel": "Sort strategy",
"sortDescription": "Override Fuse's default ordering when designers want exact or prefix matches before fuzzy ones.",
"sortExample": "Use \"Exact title first\" when Wan-branded templates must show before descriptions that merely mention \"Wan\".",
"sortModes": {
"score": "Fuse score (default)",
"exact": "Exact title or name first",
"prefix": "Prefix / word-start boost"
},
"getFnLabel": "Collection accessor",
"getFnDescription": "Control how array fields such as tags or models are flattened before scoring.",
"getFnExample": "Flatten tags/models if designers want \"Wan\" to match the model list even when the title differs.",
"getFnModes": {
"default": "Fuse default accessor",
"flatten": "Flatten arrays into readable strings"
},
"keysHeading": "Keys & weights",
"keysDescription": "Pick which template fields feed the Fuse index and how much they matter.",
"keysHelper": "Weights are relative—Fuse normalizes them automatically.",
"keysAddLabel": "Field to add",
"keysWeightLabel": "Weight",
"keysAddButton": "Add field",
"keysOptions": {
"name": "Raw name",
"title": "Localized title",
"description": "Description",
"tags": "Tags",
"models": "Models",
"useCase": "Use case",
"sourceModule": "Source module"
},
"keys": {
"nameDescription": "Matches internal slugs such as wan_image_diffusion.",
"titleDescription": "Searches the display title (localized), e.g., Wan Diffusion Starter.",
"descriptionDescription": "Looks through the marketing copy for keywords like \"face retouch\".",
"tagsDescription": "Covers curated tags (portrait, anime, product, etc.).",
"modelsDescription": "Matches referenced model names such as Wan or SDXL.",
"customDescription": "Custom weighting for {field}."
},
"removeLabel": "Remove",
"options": {
"isCaseSensitive": {
"description": "Respect letter casing when matching template text.",
"example": "Turn on if \"WAN\" should not match \"Wan Diffusion\" in lowercase."
},
"ignoreDiacritics": {
"description": "Treat accents and diacritics as plain letters.",
"example": "Let \"anime\" match templates tagged \"animé\" without needing the accent."
},
"includeScore": {
"description": "Expose the Fuse score so you can see how confident each hit is.",
"example": "Helps compare how much higher \"Wan Diffusion\" scores than \"Glow Portrait\" for the same query."
},
"includeMatches": {
"description": "Return the character indices of each match for highlighting.",
"example": "Needed when you want to visually highlight \"Wan\" inside descriptions or tags."
},
"minMatchCharLength": {
"description": "Ignore matches shorter than this many characters.",
"example": "Set to 2 so a single \"w\" typed by accident does not reshuffle the template list."
},
"shouldSort": {
"description": "Let Fuse sort by score. Turn off to keep the original grouping order.",
"example": "Disable when designers want curated ordering even while filtering by text."
},
"findAllMatches": {
"description": "Continue searching after a perfect hit to surface every occurrence.",
"example": "Use when multiple models inside the description should all highlight \"Wan\" segments."
},
"location": {
"description": "Bias toward matches near this index in the text.",
"example": "Keep near 0 if template titles should matter more than long descriptions."
},
"threshold": {
"description": "Overall fuzziness. 0 is exact, 1 matches almost anything.",
"example": "0.2 is strict enough that \"Wan\" surfaces Wan-branded templates before unrelated blurbs."
},
"distance": {
"description": "How far from the expected location a match can drift before being ignored.",
"example": "Lower values keep \"Wan\" from matching a paragraph hundreds of characters away."
},
"ignoreLocation": {
"description": "When true, disable the location + distance penalty entirely.",
"example": "Great for template data where matches can live anywhere within descriptions or tags."
},
"useExtendedSearch": {
"description": "Allow ^=, =, and other extended search syntax for power users.",
"example": "Type ^=wan to show only templates whose names start with Wan."
},
"ignoreFieldNorm": {
"description": "Ignore the penalty for long fields.",
"example": "Enable when long descriptions should not count against simple titles."
},
"fieldNormWeight": {
"description": "Scale how much field length matters in scoring.",
"example": "Set to 0 when you only care that \"Wan\" exists somewhere, no matter how wordy the field is."
}
}
},
"graphCanvasMenu": {
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
Expand Down Expand Up @@ -2392,4 +2539,4 @@
"recentReleases": "Recent releases",
"helpCenterMenu": "Help Center Menu"
}
}
}
Loading
Loading