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') }}
+
-
-
-
+
+
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') }}
- {{ $t('assetBrowser.finish') }}
+ {{
+ uploadStatus === 'processing'
+ ? $t('g.close')
+ : $t('assetBrowser.finish')
+ }}
()
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 @@
-
-
-
-
-
- {{ $t('assetBrowser.uploadingModel') }}
-
+
+
+
+ {{ $t('assetBrowser.processingModel') }}
+
+
+ {{ $t('assetBrowser.processingModelDescription') }}
+
+
+
+
![]()
+
+
+ {{ metadata?.filename || metadata?.name }}
+
+
+ {{ modelType }}
+
+
-
+
{{ $t('assetBrowser.modelUploaded') }}
@@ -47,7 +61,7 @@
@@ -66,8 +80,8 @@