diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 5e66054a0..977f3f6df 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -83,7 +83,7 @@ test.describe('Templates', () => { await comfyPage.page .locator( - 'nav > div:nth-child(2) > div > span:has-text("Getting Started")' + 'nav > div:nth-child(3) > div > span:has-text("Getting Started")' ) .click() await comfyPage.templates.loadTemplate('default') diff --git a/docs/TEMPLATE_RANKING.md b/docs/TEMPLATE_RANKING.md new file mode 100644 index 000000000..a52c8fc61 --- /dev/null +++ b/docs/TEMPLATE_RANKING.md @@ -0,0 +1,66 @@ +# Template Ranking System + +Usage-based ordering for workflow templates with position bias normalization. + +Scores are pre-computed and normalized offline and shipped as static JSON (mirrors `sorted-custom-node-map.json` pattern for node search). + +## Sort Modes + +| Mode | Formula | Description | +| -------------- | ------------------------------------------------ | ---------------------- | +| `recommended` | `usage × 0.5 + internal × 0.3 + freshness × 0.2` | Curated recommendation | +| `popular` | `usage × 0.9 + freshness × 0.1` | Pure user-driven | +| `newest` | Date sort | Existing | +| `alphabetical` | Name sort | Existing | + +Freshness computed at runtime from `template.date`: `1.0 / (1 + daysSinceAdded / 90)`, min 0.1. + +## Data Files + +**Usage scores** (generated from Mixpanel): + +```json +// In templates/index.json, add to any template: +{ + "name": "some_template", + "usage": 1000, + ... +} +``` + +**Search rank** (set per-template in workflow_templates repo): + +```json +// In templates/index.json, add to any template: +{ + "name": "some_template", + "searchRank": 8, // Scale 1-10, default 5 + ... +} +``` + +| searchRank | Effect | +| ---------- | ---------------------------- | +| 1-4 | Demote (bury in results) | +| 5 | Neutral (default if not set) | +| 6-10 | Promote (boost in results) | + +## Position Bias Correction + +Raw usage reflects true preference AND UI position bias. We use linear interpolation: + +``` +correction = 1 + (position - 1) / (maxPosition - 1) +normalizedUsage = rawUsage × correction +``` + +| Position | Boost | +| -------- | ----- | +| 1 | 1.0× | +| 50 | 1.28× | +| 100 | 1.57× | +| 175 | 2.0× | + +Templates buried at the bottom get up to 2× boost to compensate for reduced visibility. + +--- diff --git a/package.json b/package.json index 6dcf03f5b..228513ac6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.37.3", + "version": "1.37.5", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 7fc951496..551e26c62 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -9,6 +9,8 @@ @config '../../tailwind.config.ts'; +@custom-variant touch (@media (hover: none)); + @theme { --text-xxs: 0.625rem; --text-xxs--line-height: calc(1 / 0.625); diff --git a/src/components/common/TreeExplorerTreeNode.vue b/src/components/common/TreeExplorerTreeNode.vue index 186769c19..cea8ba451 100644 --- a/src/components/common/TreeExplorerTreeNode.vue +++ b/src/components/common/TreeExplorerTreeNode.vue @@ -28,7 +28,7 @@ />
diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue index 5f96fe4c2..7c76e971d 100644 --- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -175,6 +175,7 @@ { sessionStartTime.value = Date.now() }) +const systemStatsStore = useSystemStatsStore() + +const distributions = computed(() => { + // eslint-disable-next-line no-undef + switch (__DISTRIBUTION__) { + case 'cloud': + return [TemplateIncludeOnDistributionEnum.Cloud] + case 'localhost': + return [TemplateIncludeOnDistributionEnum.Local] + case 'desktop': + default: + if (systemStatsStore.systemStats?.system.os === 'darwin') { + return [ + TemplateIncludeOnDistributionEnum.Desktop, + TemplateIncludeOnDistributionEnum.Mac + ] + } + return [ + TemplateIncludeOnDistributionEnum.Desktop, + TemplateIncludeOnDistributionEnum.Windows + ] + } +}) + // Wrap onClose to track session end const onClose = () => { if (isCloud) { @@ -511,6 +538,9 @@ const allTemplates = computed(() => { return workflowTemplatesStore.enhancedTemplates }) +// Navigation +const selectedNavItem = ref('all') + // Filter templates based on selected navigation item const navigationFilteredTemplates = computed(() => { if (!selectedNavItem.value) { @@ -536,6 +566,36 @@ const { resetFilters } = useTemplateFiltering(navigationFilteredTemplates) +/** + * Coordinates state between the selected navigation item and the sort order to + * create deterministic, predictable behavior. + * @param source The origin of the change ('nav' or 'sort'). + */ +const coordinateNavAndSort = (source: 'nav' | 'sort') => { + const isPopularNav = selectedNavItem.value === 'popular' + const isPopularSort = sortBy.value === 'popular' + + if (source === 'nav') { + if (isPopularNav && !isPopularSort) { + // When navigating to 'Popular' category, automatically set sort to 'Popular'. + sortBy.value = 'popular' + } else if (!isPopularNav && isPopularSort) { + // When navigating away from 'Popular' category while sort is 'Popular', reset sort to default. + sortBy.value = 'default' + } + } else if (source === 'sort') { + // When sort is changed away from 'Popular' while in the 'Popular' category, + // reset the category to 'All Templates' to avoid a confusing state. + if (isPopularNav && !isPopularSort) { + selectedNavItem.value = 'all' + } + } +} + +// Watch for changes from the two sources ('nav' and 'sort') and trigger the coordinator. +watch(selectedNavItem, () => coordinateNavAndSort('nav')) +watch(sortBy, () => coordinateNavAndSort('sort')) + // Convert between string array and object array for MultiSelect component const selectedModelObjects = computed({ get() { @@ -578,9 +638,6 @@ const cardRefs = ref([]) // Force re-render key for templates when sorting changes const templateListKey = ref(0) -// Navigation -const selectedNavItem = ref('all') - // Search text for model filter const modelSearchText = ref('') @@ -645,11 +702,19 @@ const runsOnFilterLabel = computed(() => { // Sort options const sortOptions = computed(() => [ - { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' }, { name: t('templateWorkflows.sort.default', 'Default'), value: 'default' }, + { + name: t('templateWorkflows.sort.recommended', 'Recommended'), + value: 'recommended' + }, + { + name: t('templateWorkflows.sort.popular', 'Popular'), + value: 'popular' + }, + { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' }, { name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Usage (Low to High)'), value: 'vram-low-to-high' @@ -750,7 +815,7 @@ const pageTitle = computed(() => { // Initialize templates loading with useAsyncState const { isLoading } = useAsyncState( async () => { - // Run both operations in parallel for better performance + // Run all operations in parallel for better performance await Promise.all([ loadTemplates(), workflowTemplatesStore.loadWorkflowTemplates() @@ -763,6 +828,14 @@ const { isLoading } = useAsyncState( } ) +const isTemplateVisibleOnDistribution = (template: TemplateInfo) => { + return (template.includeOnDistributions?.length ?? 0) > 0 + ? distributions.value.some((d) => + template.includeOnDistributions?.includes(d) + ) + : true +} + onBeforeUnmount(() => { cardRefs.value = [] // Release DOM refs }) diff --git a/src/components/dialog/content/TopUpCreditsDialogContent.vue b/src/components/dialog/content/TopUpCreditsDialogContent.vue index 6985aa7ad..ce061300a 100644 --- a/src/components/dialog/content/TopUpCreditsDialogContent.vue +++ b/src/components/dialog/content/TopUpCreditsDialogContent.vue @@ -49,25 +49,66 @@ @select="selectedCredits = option.credits" /> -
- {{ $t('credits.topUp.templateNote') }} +
+ + + {{ t('subscription.videoTemplateBasedCredits') }} +
-
- - + + - {{ $t('credits.topUp.buy') }} - +
+

+ {{ t('subscription.videoEstimateExplanation') }} +

+ + + {{ t('subscription.videoEstimateTryTemplate') }} + + + +
+
diff --git a/src/platform/assets/components/UploadModelDialog.vue b/src/platform/assets/components/UploadModelDialog.vue index d6be9e97e..3a7c76bfe 100644 --- a/src/platform/assets/components/UploadModelDialog.vue +++ b/src/platform/assets/components/UploadModelDialog.vue @@ -25,8 +25,8 @@ {{ $t('assetBrowser.upload') }} () const emit = defineEmits<{ diff --git a/src/platform/assets/components/UploadModelProgress.vue b/src/platform/assets/components/UploadModelProgress.vue index 839b5d3b4..e541e9bc0 100644 --- a/src/platform/assets/components/UploadModelProgress.vue +++ b/src/platform/assets/components/UploadModelProgress.vue @@ -1,22 +1,36 @@