mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-13 18:59:32 +00:00
## Summary The template picker's search now surfaces the right template for how people actually type — abbreviations, typos, multi-word intent, and non-Latin (CJK) titles — and orders results by real popularity instead of a fuzzy-match score that was being thrown away. Search and ranking now behave the same here as they do on the workflow hub. ## Changes **What** - Searching for the way people phrase things now works: `t2v`, `i2v`, `cn` expand to their full modality terms, `img2img`/`v2v` expand to editing (matching how the catalog tags image/video edit templates), `flux upscale` and `sdxl lora` match across title/model/tag fields together, prefixes like `vid` match `video`, and typos like `contorlnet` still find ControlNet. Versioned names tokenize sensibly, so `wan 2.2` and `wan2.2` both hit, while `2.5` never blurs into `3.5`. - CJK titles are searchable. Unspaced Han/Hiragana/Katakana runs are tokenized into character unigrams and bigrams, so a substring a user types (`放大` inside `图像放大`, or the single trailing `大`) lands on a match. Korean and other spaced scripts fall to the normal word tokenizer, unchanged. - Fuzzy matching is tighter: a term now tolerates edits up to 20% of its length (down from a flat threshold), so `contorlnet` still finds ControlNet but `upscale` no longer fuzzy-matches the shorter, unrelated `scale`. Short (≤3-char) and digit-bearing terms stay exact. - Results lead with text relevance. Previously the fuzzy match score was computed and then discarded, and any active sort re-ordered results by usage — so the best textual match rarely landed on top. Now relevance is the authoritative order while a query is active, and when two results match about equally well, the more-used template wins the tie (dampened so one runaway-popular template can't dominate). - The ranking is a stable total order. Scores are bucketed before usage breaks ties, so a cluster of near-equally-relevant results always sorts the same way — a naive per-pair "within X%" comparison is intransitive and makes the order depend on internal input order (it can even shuffle as you type another character). - "Popular" ranks by raw usage, matching what the hub and the search index show. It previously blended in a freshness term that pushed newer, less-used templates above genuinely popular ones. - The sort dropdown works during search again: it defaults to "Relevance" but you can switch to Popular/Newest/etc. to re-order the results, and your browse sort is restored (and never overwritten by a search-time choice) when you clear the query. - Alphabetical sort reads correctly: it sorts by the title shown on the card, trims stray leading whitespace that used to jump templates to the top, and groups number-prefixed titles after the letters instead of ahead of them. - Filter telemetry now reports the sort the user is actually seeing (relevance while searching) rather than the persisted browse sort, so analytics reflect the visible ordering. - Removed the old runtime Fuse-options override path, which is obsolete under the new engine. **Breaking** None. Existing filters (Model / Use Case / Runs On / distribution), pagination, and persisted sort settings are unchanged; the relevance mode is search-only and never persisted. ## Review Focus - The ranking crux is `rankByRelevanceThenUsage` in `templateSearchConfig.ts`: relevance is primary, usage only re-orders results in the same score bucket, and bucketing keeps it a stable total order. That's the one function to review for correctness. - The CJK tokenizer (`cjkGrams` / `tokenize` in `templateSearchConfig.ts`): script-matched so only unspaced scripts are grammed, and a pure-CJK run relies on its grams (no whole-word token). Splitting by code point is safe here (these scripts are BMP-only; emoji are excluded by the run regex). - Deliberately not touched: the "Recommended" sort keeps its curated blend (usage + editorial rank + freshness) so it stays distinct from "Popular"; `vram-low-to-high` remains unimplemented exactly as on main. ## Tradeoffs / notes - Adds `minisearch` (~18 kB gzip). The template selector is where it's used; accepted for the search-quality gain (a later change could lazy-load it if bundle size becomes a concern). - Bucketing means two results just across a bucket boundary don't tie-break on usage even when their scores are close — the accepted cost of a transitive, predictable order (this mirrors how the search index quantizes relevance). - `img2img` expands to editing (not literal "image to image") because the catalog labels those templates "Image Edit" — verified against the real data. - CJK bigrams roughly double the token count for a pure-CJK title; negligible at catalog scale (~550 templates, short titles). ## Testing Behavioral coverage over the real search paths, not the mocks — the ranking and tokenizer run against actual MiniSearch output; only the ranking-store math is mocked. Also verified against the full ~550-template catalog end to end (all query types stable, zero input-order-dependent orderings). ### Behavior matrix (verified on the real catalog) | Input / action | Now | Previously | | --- | --- | --- | | `img2img` | Image-editing templates (Qwen Image Edit, …) | Matched every "image" template — intent lost | | `flux upscale` | Flux upscale templates (matches both terms across fields) | **No results** (single-field fuzzy couldn't span title + tag) | | `sdxl lora` | SDXL templates | **No results** | | `t2v` / `i2v` / `cn` | Expand to text→video / image→video / controlnet | Only partial slug hits, if any | | `vid` (prefix) | Matches `video` templates | Unreliable | | `contorlnet` (typo) | Finds ControlNet | Often dropped by the strict threshold | | `upscale` | Matches upscale titles only | Fuzzy-matched the unrelated substring `scale` | | `放大` / `大` (CJK) | Matches `图像放大` and other titles containing the run | No match — CJK titles were unsearchable by substring | | `wan 2.2` and `wan2.2` | Both match; `2.5` never matches `3.5` | Space vs no-space degraded the match | | Near-tied cluster (e.g. `upscale`) | Stable order every time | Reordered depending on input order (could shuffle as you type) | | Query active, "Popular" selected | Best textual match still leads; usage breaks near-ties | Sort re-ordered by usage, burying the best match | | Change sort while searching | Re-orders the search results; relevance is the default | Sort was locked; dropdown had no effect | | Clear the search | Restores the browse sort you had before | — | | "Popular" sort | Orders by raw usage (matches hub / index) | Freshness blend pushed newer low-usage templates up | | A–Z sort | Letters first (`ACE…`), number-prefixed titles last (`3x3…`, `360…`); leading whitespace ignored | Leading-space titles jumped to the top; numbers sorted before letters | ### Unit tests (81 total, all passing) - `templateSearchConfig.test.ts` (34) — tokenizer identifier/version splits, CJK unigram/bigram gramming (and Korean left as a spaced word), per-term fuzziness (`upscale` ≠ `scale`), abbreviation expansion (incl. `img2img`→edit intent), prefix + typo matching, AND-then-OR, literal-before-expansion ordering, relevance>tag>description ranking, and `rankByRelevanceThenUsage` giving a stable order on an intransitive cluster. - `useTemplateFiltering.test.ts` (35) — the `img2img` / `flux upscale` / `sdxl lora` regressions, relevance-default-on-search, override-sort-while-searching, browse-sort restore on clear, ephemeral mid-search sort, telemetry reporting the visible sort, Runs-On filter, empty-result guard, filters preserving relevance order, and alphabetical trimming + numbers-after-letters. - `templateRankingStore.test.ts` (12) — freshness and default-score (recommended) math. Gate: `pnpm typecheck`, `pnpm lint`, `pnpm knip` clean. ## Screen Recording (if applicable) https://github.com/user-attachments/assets/6748a3f7-e69d-44ac-826c-71990c8dce90
221 lines
9.2 KiB
JSON
221 lines
9.2 KiB
JSON
{
|
|
"name": "@comfyorg/comfyui-frontend",
|
|
"version": "1.48.1",
|
|
"private": true,
|
|
"description": "Official front-end implementation of ComfyUI",
|
|
"homepage": "https://comfy.org",
|
|
"license": "GPL-3.0-only",
|
|
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
|
"type": "module",
|
|
"scripts": {
|
|
"build:cloud": "cross-env DISTRIBUTION=cloud vite build --config vite.config.mts",
|
|
"build:desktop": "pnpm --filter @comfyorg/desktop-ui run build",
|
|
"build-storybook": "storybook build",
|
|
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
|
|
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
|
|
"build": "pnpm typecheck && vite build --config vite.config.mts",
|
|
"clean": "pnpm dlx rimraf dist dist-ssr coverage playwright-report blob-report test-results node_modules/.vite apps/desktop-ui/dist apps/website/dist",
|
|
"clean:all": "pnpm clean && pnpm dlx rimraf node_modules",
|
|
"size:collect": "node scripts/size-collect.js",
|
|
"size:report": "node scripts/size-report.js",
|
|
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
|
"dev:cloud": "pnpm dev:cloud:test",
|
|
"dev:cloud:test": "cross-env DEV_SERVER_COMFYUI_URL=https://testcloud.comfy.org/ vite --config vite.config.mts",
|
|
"dev:cloud:staging": "cross-env DEV_SERVER_COMFYUI_URL=https://stagingcloud.comfy.org/ vite --config vite.config.mts",
|
|
"dev:cloud:prod": "cross-env DEV_SERVER_COMFYUI_URL=https://cloud.comfy.org/ vite --config vite.config.mts",
|
|
"dev:desktop": "pnpm --filter @comfyorg/desktop-ui run dev",
|
|
"dev:electron": "cross-env DISTRIBUTION=desktop vite --config vite.electron.config.mts",
|
|
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true vite --config vite.config.mts",
|
|
"dev:test": "cross-env VITE_USE_LEGACY_DEFAULT_GRAPH=true vite --config vite.config.mts",
|
|
"dev": "vite --config vite.config.mts",
|
|
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
|
"format:check": "oxfmt --check",
|
|
"format": "oxfmt --write",
|
|
"json-schema": "tsx scripts/generate-json-schema.ts",
|
|
"knip:no-cache": "knip",
|
|
"knip": "knip --cache",
|
|
"lint:fix:no-cache": "oxlint src browser_tests --type-aware --fix && eslint src --fix",
|
|
"lint:fix": "oxlint src browser_tests --type-aware --fix && eslint src --cache --fix",
|
|
"lint:no-cache": "pnpm exec stylelint '{apps,packages,src}/**/*.{css,vue}' && oxlint src browser_tests --type-aware && eslint src",
|
|
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
|
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
|
"lint": "pnpm stylelint && oxlint src browser_tests --type-aware && eslint src --cache",
|
|
"lint:desktop": "pnpm --filter @comfyorg/desktop-ui run lint",
|
|
"locale": "lobe-i18n locale",
|
|
"oxlint": "oxlint src browser_tests --type-aware",
|
|
"prepare": "pnpm exec husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
|
"preview": "vite preview --config vite.config.mts",
|
|
"storybook": "storybook dev -p 6006",
|
|
"storybook:desktop": "pnpm --filter @comfyorg/desktop-ui run storybook",
|
|
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
|
|
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
|
|
"test:browser": "pnpm exec playwright test",
|
|
"test:browser:coverage": "cross-env COLLECT_COVERAGE=true pnpm test:browser",
|
|
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
|
|
"test:coverage": "vitest run --coverage",
|
|
"test:unit": "vitest run",
|
|
"typecheck": "vue-tsc --noEmit",
|
|
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",
|
|
"typecheck:desktop": "pnpm --filter @comfyorg/desktop-ui run typecheck",
|
|
"typecheck:website": "pnpm --filter @comfyorg/website run typecheck",
|
|
"zipdist": "node scripts/zipdist.js"
|
|
},
|
|
"dependencies": {
|
|
"@alloc/quick-lru": "catalog:",
|
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
|
"@comfyorg/comfyui-desktop-bridge-types": "workspace:*",
|
|
"@comfyorg/comfyui-electron-types": "catalog:",
|
|
"@comfyorg/design-system": "workspace:*",
|
|
"@comfyorg/fbx-exporter-three": "^1.0.1",
|
|
"@comfyorg/object-info-parser": "workspace:*",
|
|
"@comfyorg/registry-types": "workspace:*",
|
|
"@comfyorg/shared-frontend-utils": "workspace:*",
|
|
"@comfyorg/tailwind-utils": "workspace:*",
|
|
"@customerio/cdp-analytics-browser": "catalog:",
|
|
"@formkit/auto-animate": "catalog:",
|
|
"@iconify/json": "catalog:",
|
|
"@primeuix/forms": "catalog:",
|
|
"@primeuix/styled": "catalog:",
|
|
"@primeuix/utils": "catalog:",
|
|
"@primevue/core": "catalog:",
|
|
"@primevue/forms": "catalog:",
|
|
"@primevue/icons": "catalog:",
|
|
"@primevue/themes": "catalog:",
|
|
"@sentry/vue": "catalog:",
|
|
"@sparkjsdev/spark": "catalog:",
|
|
"@tanstack/vue-virtual": "catalog:",
|
|
"@tiptap/core": "catalog:",
|
|
"@tiptap/extension-link": "catalog:",
|
|
"@tiptap/extension-table": "catalog:",
|
|
"@tiptap/extension-table-cell": "catalog:",
|
|
"@tiptap/extension-table-header": "catalog:",
|
|
"@tiptap/extension-table-row": "catalog:",
|
|
"@tiptap/pm": "catalog:",
|
|
"@tiptap/starter-kit": "catalog:",
|
|
"@vee-validate/zod": "catalog:",
|
|
"@vueuse/core": "catalog:",
|
|
"@vueuse/integrations": "catalog:",
|
|
"@vueuse/router": "^14.2.0",
|
|
"@xterm/addon-fit": "^0.10.0",
|
|
"@xterm/addon-serialize": "^0.13.0",
|
|
"@xterm/xterm": "^5.5.0",
|
|
"algoliasearch": "catalog:",
|
|
"axios": "catalog:",
|
|
"chart.js": "^4.5.0",
|
|
"cva": "catalog:",
|
|
"dompurify": "catalog:",
|
|
"dotenv": "catalog:",
|
|
"es-toolkit": "^1.39.9",
|
|
"extendable-media-recorder": "^9.2.27",
|
|
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
|
"firebase": "catalog:",
|
|
"fuse.js": "^7.0.0",
|
|
"glob": "catalog:",
|
|
"jsonata": "catalog:",
|
|
"loglevel": "^1.9.2",
|
|
"marked": "^15.0.11",
|
|
"minisearch": "catalog:",
|
|
"pinia": "catalog:",
|
|
"posthog-js": "catalog:",
|
|
"primeicons": "catalog:",
|
|
"primevue": "catalog:",
|
|
"reka-ui": "catalog:",
|
|
"semver": "^7.7.2",
|
|
"three": "catalog:",
|
|
"tiptap-markdown": "^0.8.10",
|
|
"typegpu": "catalog:",
|
|
"vee-validate": "catalog:",
|
|
"vue": "catalog:",
|
|
"vue-i18n": "catalog:",
|
|
"vue-router": "catalog:",
|
|
"vuefire": "catalog:",
|
|
"wwobjloader2": "catalog:",
|
|
"yjs": "catalog:",
|
|
"zod": "catalog:",
|
|
"zod-validation-error": "catalog:"
|
|
},
|
|
"devDependencies": {
|
|
"@comfyorg/ingest-types": "workspace:*",
|
|
"@eslint/js": "catalog:",
|
|
"@intlify/eslint-plugin-vue-i18n": "catalog:",
|
|
"@lobehub/i18n-cli": "catalog:",
|
|
"@pinia/testing": "catalog:",
|
|
"@playwright/test": "catalog:",
|
|
"@sentry/vite-plugin": "catalog:",
|
|
"@storybook/addon-docs": "catalog:",
|
|
"@storybook/addon-mcp": "catalog:",
|
|
"@storybook/vue3": "catalog:",
|
|
"@storybook/vue3-vite": "catalog:",
|
|
"@tailwindcss/vite": "catalog:",
|
|
"@testing-library/jest-dom": "catalog:",
|
|
"@testing-library/user-event": "catalog:",
|
|
"@testing-library/vue": "catalog:",
|
|
"@total-typescript/shoehorn": "catalog:",
|
|
"@types/fs-extra": "catalog:",
|
|
"@types/jsdom": "catalog:",
|
|
"@types/node": "catalog:",
|
|
"@types/semver": "catalog:",
|
|
"@types/three": "catalog:",
|
|
"@vitejs/plugin-vue": "catalog:",
|
|
"@vitest/coverage-v8": "catalog:",
|
|
"@vitest/ui": "catalog:",
|
|
"@webgpu/types": "catalog:",
|
|
"cross-env": "catalog:",
|
|
"eslint": "catalog:",
|
|
"eslint-config-prettier": "catalog:",
|
|
"eslint-import-resolver-typescript": "catalog:",
|
|
"eslint-plugin-better-tailwindcss": "catalog:",
|
|
"eslint-plugin-import-x": "catalog:",
|
|
"eslint-plugin-oxlint": "catalog:",
|
|
"eslint-plugin-playwright": "catalog:",
|
|
"eslint-plugin-storybook": "catalog:",
|
|
"eslint-plugin-testing-library": "catalog:",
|
|
"eslint-plugin-unused-imports": "catalog:",
|
|
"eslint-plugin-vue": "catalog:",
|
|
"fast-check": "catalog:",
|
|
"fs-extra": "^11.2.0",
|
|
"globals": "catalog:",
|
|
"happy-dom": "catalog:",
|
|
"husky": "catalog:",
|
|
"jiti": "catalog:",
|
|
"jsdom": "catalog:",
|
|
"knip": "catalog:",
|
|
"lint-staged": "catalog:",
|
|
"markdown-table": "catalog:",
|
|
"mixpanel-browser": "catalog:",
|
|
"monocart-coverage-reports": "catalog:",
|
|
"oxfmt": "catalog:",
|
|
"oxlint": "catalog:",
|
|
"oxlint-tsgolint": "catalog:",
|
|
"picocolors": "catalog:",
|
|
"postcss-html": "catalog:",
|
|
"pretty-bytes": "catalog:",
|
|
"rollup-plugin-visualizer": "catalog:",
|
|
"storybook": "catalog:",
|
|
"stylelint": "catalog:",
|
|
"tailwindcss": "catalog:",
|
|
"tsx": "catalog:",
|
|
"typescript": "catalog:",
|
|
"typescript-eslint": "catalog:",
|
|
"unplugin-icons": "catalog:",
|
|
"unplugin-typegpu": "catalog:",
|
|
"unplugin-vue-components": "catalog:",
|
|
"uuid": "catalog:",
|
|
"vite": "catalog:",
|
|
"vite-plugin-dts": "catalog:",
|
|
"vite-plugin-html": "catalog:",
|
|
"vite-plugin-vue-devtools": "catalog:",
|
|
"vitest": "catalog:",
|
|
"vue-component-type-helpers": "catalog:",
|
|
"vue-eslint-parser": "catalog:",
|
|
"vue-tsc": "catalog:",
|
|
"zip-dir": "^2.0.0",
|
|
"zod-to-json-schema": "catalog:"
|
|
},
|
|
"engines": {
|
|
"node": ">=25 <26",
|
|
"pnpm": ">=11.3"
|
|
},
|
|
"packageManager": "pnpm@11.3.0"
|
|
}
|