mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 01:39:47 +00:00
refactor: let categories on leftsidepanel be one to one with JSON
This commit is contained in:
@@ -2,14 +2,13 @@ import Fuse from 'fuse.js'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref, shallowRef } from 'vue'
|
import { computed, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
import { SMALL_MODEL_SIZE_LIMIT } from '@/constants/templateWorkflows'
|
|
||||||
import { i18n, st } from '@/i18n'
|
import { i18n, st } from '@/i18n'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||||
import { getCategoryIcon } from '@/utils/categoryIcons'
|
import { getCategoryIcon } from '@/utils/categoryIcons'
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
TemplateGroup,
|
TemplateGroup,
|
||||||
TemplateInfo,
|
TemplateInfo,
|
||||||
WorkflowTemplates
|
WorkflowTemplates
|
||||||
@@ -20,9 +19,8 @@ interface EnhancedTemplate extends TemplateInfo {
|
|||||||
sourceModule: string
|
sourceModule: string
|
||||||
category?: string
|
category?: string
|
||||||
categoryType?: string
|
categoryType?: string
|
||||||
isAPI?: boolean
|
categoryGroup?: string // 'GENERATION TYPE' or 'CLOSED SOURCE MODELS'
|
||||||
isPerformance?: boolean
|
isEssential?: boolean
|
||||||
isMacCompatible?: boolean
|
|
||||||
searchableText?: string
|
searchableText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +31,14 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
const coreTemplates = shallowRef<WorkflowTemplates[]>([])
|
const coreTemplates = shallowRef<WorkflowTemplates[]>([])
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
|
// Store filter mappings for dynamic categories
|
||||||
|
type FilterData = {
|
||||||
|
category?: string
|
||||||
|
categoryGroup?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryFilters = ref(new Map<string, FilterData>())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add localization fields to a template.
|
* Add localization fields to a template.
|
||||||
*/
|
*/
|
||||||
@@ -182,39 +188,13 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
// Process core templates
|
// Process core templates
|
||||||
coreTemplates.value.forEach((category) => {
|
coreTemplates.value.forEach((category) => {
|
||||||
category.templates.forEach((template) => {
|
category.templates.forEach((template) => {
|
||||||
const isAPI = category.title?.includes('API') || false
|
|
||||||
// Determine performance ("Small Models") based primarily on explicit size prop (<=3GB)
|
|
||||||
// Fallback to heuristic based on model name keywords for backward compatibility.
|
|
||||||
const explicitSize = template.size
|
|
||||||
const heuristicPerformance = template.models?.some(
|
|
||||||
(model) =>
|
|
||||||
model.toLowerCase().includes('turbo') ||
|
|
||||||
model.toLowerCase().includes('fast') ||
|
|
||||||
model.toLowerCase().includes('schnell') ||
|
|
||||||
model.toLowerCase().includes('fp8')
|
|
||||||
)
|
|
||||||
const isPerformance =
|
|
||||||
(typeof explicitSize === 'number' &&
|
|
||||||
explicitSize <= SMALL_MODEL_SIZE_LIMIT) ||
|
|
||||||
(!!explicitSize === false && heuristicPerformance) ||
|
|
||||||
false
|
|
||||||
|
|
||||||
const isMacCompatible =
|
|
||||||
template.models?.some(
|
|
||||||
(model) =>
|
|
||||||
model.toLowerCase().includes('fp8') ||
|
|
||||||
model.toLowerCase().includes('turbo') ||
|
|
||||||
model.toLowerCase().includes('schnell')
|
|
||||||
) || false
|
|
||||||
|
|
||||||
const enhancedTemplate: EnhancedTemplate = {
|
const enhancedTemplate: EnhancedTemplate = {
|
||||||
...template,
|
...template,
|
||||||
sourceModule: category.moduleName,
|
sourceModule: category.moduleName,
|
||||||
category: category.title,
|
category: category.title,
|
||||||
categoryType: category.type,
|
categoryType: category.type,
|
||||||
isAPI,
|
categoryGroup: category.category,
|
||||||
isPerformance,
|
isEssential: category.isEssential,
|
||||||
isMacCompatible,
|
|
||||||
searchableText: [
|
searchableText: [
|
||||||
template.title || template.name,
|
template.title || template.name,
|
||||||
template.description || '',
|
template.description || '',
|
||||||
@@ -241,9 +221,6 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
sourceModule: moduleName,
|
sourceModule: moduleName,
|
||||||
category: 'Extensions',
|
category: 'Extensions',
|
||||||
categoryType: 'extension',
|
categoryType: 'extension',
|
||||||
isAPI: false,
|
|
||||||
isPerformance: false,
|
|
||||||
isMacCompatible: false,
|
|
||||||
searchableText: `${name} ${moduleName} extension`
|
searchableText: `${name} ${moduleName} extension`
|
||||||
}
|
}
|
||||||
allTemplates.push(enhancedTemplate)
|
allTemplates.push(enhancedTemplate)
|
||||||
@@ -273,196 +250,145 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter templates by category using Fuse.js
|
* Filter templates by category ID using stored filter mappings
|
||||||
*/
|
*/
|
||||||
const filterTemplatesByCategory = (categoryId: string) => {
|
const filterTemplatesByCategory = (categoryId: string) => {
|
||||||
if (categoryId === 'all') {
|
if (categoryId === 'all') {
|
||||||
return enhancedTemplates.value
|
return enhancedTemplates.value
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (categoryId) {
|
if (categoryId === 'basics') {
|
||||||
case 'getting-started':
|
// Filter for templates from categories marked as essential
|
||||||
return enhancedTemplates.value.filter((t) => t.category === 'Basics')
|
return enhancedTemplates.value.filter((t) => t.isEssential)
|
||||||
|
|
||||||
case 'generation-image':
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === 'image' && !t.isAPI
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'generation-video':
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === 'video' && !t.isAPI
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'generation-3d':
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === '3d' && !t.isAPI
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'generation-audio':
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === 'audio' && !t.isAPI
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'generation-llm':
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.tags?.includes('LLM') || t.tags?.includes('Chat')
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'api-nodes':
|
|
||||||
return enhancedTemplates.value.filter((t) => t.isAPI)
|
|
||||||
|
|
||||||
case 'extensions':
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.sourceModule !== 'default'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Removed lora-training filter (deprecated)
|
|
||||||
|
|
||||||
case 'performance-small':
|
|
||||||
case 'performance-mac':
|
|
||||||
return enhancedTemplates.value // deprecated filters; return all
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Handle extension-specific filters
|
|
||||||
if (categoryId.startsWith('extension-')) {
|
|
||||||
const moduleName = categoryId.replace('extension-', '')
|
|
||||||
return enhancedTemplates.value.filter(
|
|
||||||
(t) => t.sourceModule === moduleName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return enhancedTemplates.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle extension-specific filters
|
||||||
|
if (categoryId.startsWith('extension-')) {
|
||||||
|
const moduleName = categoryId.replace('extension-', '')
|
||||||
|
return enhancedTemplates.value.filter(
|
||||||
|
(t) => t.sourceModule === moduleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the filter from our stored mappings
|
||||||
|
const filter = categoryFilters.value.get(categoryId)
|
||||||
|
if (!filter) {
|
||||||
|
return enhancedTemplates.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the filter
|
||||||
|
return enhancedTemplates.value.filter((template) => {
|
||||||
|
if (filter.category && template.category !== filter.category) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
filter.categoryGroup &&
|
||||||
|
template.categoryGroup !== filter.categoryGroup
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New navigation structure matching NavItemData | NavGroupData format
|
* New navigation structure dynamically built from JSON categories
|
||||||
*/
|
*/
|
||||||
const navGroupedTemplates = computed<(NavItemData | NavGroupData)[]>(() => {
|
const navGroupedTemplates = computed<(NavItemData | NavGroupData)[]>(() => {
|
||||||
if (!isLoaded.value) return []
|
if (!isLoaded.value) return []
|
||||||
|
|
||||||
const items: (NavItemData | NavGroupData)[] = []
|
const items: (NavItemData | NavGroupData)[] = []
|
||||||
|
|
||||||
// Count templates for each category
|
// Clear and rebuild filter mappings
|
||||||
const imageCounts = enhancedTemplates.value.filter(
|
categoryFilters.value.clear()
|
||||||
(t) => t.categoryType === 'image' && !t.isAPI
|
|
||||||
).length
|
|
||||||
const videoCounts = enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === 'video' && !t.isAPI
|
|
||||||
).length
|
|
||||||
const audioCounts = enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === 'audio' && !t.isAPI
|
|
||||||
).length
|
|
||||||
const llmCounts = enhancedTemplates.value.filter(
|
|
||||||
(t) => t.tags?.includes('LLM') || t.tags?.includes('Chat')
|
|
||||||
).length
|
|
||||||
const threeDCounts = enhancedTemplates.value.filter(
|
|
||||||
(t) => t.categoryType === '3d' && !t.isAPI
|
|
||||||
).length
|
|
||||||
const apiCounts = enhancedTemplates.value.filter((t) => t.isAPI).length
|
|
||||||
const gettingStartedCounts = enhancedTemplates.value.filter(
|
|
||||||
(t) => t.category === 'Basics'
|
|
||||||
).length
|
|
||||||
const extensionCounts = enhancedTemplates.value.filter(
|
|
||||||
(t) => t.sourceModule !== 'default'
|
|
||||||
).length
|
|
||||||
|
|
||||||
// All Templates - as a simple selector
|
// 1. All Templates - always first
|
||||||
items.push({
|
items.push({
|
||||||
id: 'all',
|
id: 'all',
|
||||||
label: st('templateWorkflows.category.All', 'All Templates'),
|
label: st('templateWorkflows.category.All', 'All Templates'),
|
||||||
icon: getCategoryIcon('all')
|
icon: getCategoryIcon('all')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Getting Started - as a simple selector
|
// 2. Basics (isEssential categories) - always second if it exists
|
||||||
if (gettingStartedCounts > 0) {
|
const hasEssentialCategories = coreTemplates.value.some(
|
||||||
|
(cat) => cat.isEssential && cat.templates.length > 0
|
||||||
|
)
|
||||||
|
if (hasEssentialCategories) {
|
||||||
items.push({
|
items.push({
|
||||||
id: 'getting-started',
|
id: 'basics',
|
||||||
label: st(
|
label: st('templateWorkflows.category.Basics', 'Basics'),
|
||||||
'templateWorkflows.category.GettingStarted',
|
icon: 'icon-[lucide--graduation-cap]'
|
||||||
'Getting Started'
|
|
||||||
),
|
|
||||||
icon: getCategoryIcon('getting-started')
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generation Type - as a group with sub-items
|
// 3. Group categories from JSON dynamically
|
||||||
if (
|
const categoryGroups = new Map<
|
||||||
imageCounts > 0 ||
|
string,
|
||||||
videoCounts > 0 ||
|
{ title: string; items: NavItemData[] }
|
||||||
threeDCounts > 0 ||
|
>()
|
||||||
audioCounts > 0 ||
|
|
||||||
llmCounts > 0
|
|
||||||
) {
|
|
||||||
const generationTypeItems: NavItemData[] = []
|
|
||||||
|
|
||||||
if (imageCounts > 0) {
|
// Process all categories from JSON
|
||||||
generationTypeItems.push({
|
coreTemplates.value.forEach((category) => {
|
||||||
id: 'generation-image',
|
// Skip essential categories as they're handled as Basics
|
||||||
label: st('templateWorkflows.category.Image', 'Image'),
|
if (category.isEssential) return
|
||||||
icon: getCategoryIcon('generation-image')
|
|
||||||
|
const categoryGroup = category.category
|
||||||
|
const categoryIcon = category.icon
|
||||||
|
|
||||||
|
if (categoryGroup) {
|
||||||
|
if (!categoryGroups.has(categoryGroup)) {
|
||||||
|
categoryGroups.set(categoryGroup, {
|
||||||
|
title: categoryGroup,
|
||||||
|
items: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = categoryGroups.get(categoryGroup)!
|
||||||
|
|
||||||
|
// Generate unique ID for this category
|
||||||
|
const categoryId = `${categoryGroup.toLowerCase().replace(/\s+/g, '-')}-${category.title.toLowerCase().replace(/\s+/g, '-')}`
|
||||||
|
|
||||||
|
// Store the filter mapping
|
||||||
|
categoryFilters.value.set(categoryId, {
|
||||||
|
category: category.title,
|
||||||
|
categoryGroup: categoryGroup
|
||||||
|
})
|
||||||
|
|
||||||
|
group.items.push({
|
||||||
|
id: categoryId,
|
||||||
|
label: st(
|
||||||
|
`templateWorkflows.category.${normalizeI18nKey(category.title)}`,
|
||||||
|
category.title
|
||||||
|
),
|
||||||
|
icon: categoryIcon || getCategoryIcon(category.type || 'default')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (videoCounts > 0) {
|
// Add grouped categories
|
||||||
generationTypeItems.push({
|
categoryGroups.forEach((group, groupName) => {
|
||||||
id: 'generation-video',
|
if (group.items.length > 0) {
|
||||||
label: st('templateWorkflows.category.Video', 'Video'),
|
items.push({
|
||||||
icon: getCategoryIcon('generation-video')
|
title: st(
|
||||||
|
`templateWorkflows.category.${normalizeI18nKey(groupName)}`,
|
||||||
|
groupName
|
||||||
|
.split(' ')
|
||||||
|
.map(
|
||||||
|
(word) =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||||
|
)
|
||||||
|
.join(' ')
|
||||||
|
),
|
||||||
|
items: group.items
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (threeDCounts > 0) {
|
// 4. Extensions - always last
|
||||||
generationTypeItems.push({
|
const extensionCounts = enhancedTemplates.value.filter(
|
||||||
id: 'generation-3d',
|
(t) => t.sourceModule !== 'default'
|
||||||
label: st('templateWorkflows.category.3DModels', '3D Models'),
|
).length
|
||||||
icon: getCategoryIcon('generation-3d')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioCounts > 0) {
|
|
||||||
generationTypeItems.push({
|
|
||||||
id: 'generation-audio',
|
|
||||||
label: st('templateWorkflows.category.Audio', 'Audio'),
|
|
||||||
icon: getCategoryIcon('generation-audio')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (llmCounts > 0) {
|
|
||||||
generationTypeItems.push({
|
|
||||||
id: 'generation-llm',
|
|
||||||
label: st('templateWorkflows.category.LLMs', 'LLMs'),
|
|
||||||
icon: getCategoryIcon('generation-llm')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
title: st(
|
|
||||||
'templateWorkflows.category.GenerationType',
|
|
||||||
'Generation Type'
|
|
||||||
),
|
|
||||||
items: generationTypeItems
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closed Models (API nodes) - as a group
|
|
||||||
if (apiCounts > 0) {
|
|
||||||
items.push({
|
|
||||||
title: st(
|
|
||||||
'templateWorkflows.category.ClosedSourceModels',
|
|
||||||
'Closed Source Models'
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: 'api-nodes',
|
|
||||||
label: st('templateWorkflows.category.APINodes', 'API nodes'),
|
|
||||||
icon: getCategoryIcon('api-nodes')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extensions - as a group with sub-items
|
|
||||||
if (extensionCounts > 0) {
|
if (extensionCounts > 0) {
|
||||||
// Get unique extension modules
|
// Get unique extension modules
|
||||||
const extensionModules = Array.from(
|
const extensionModules = Array.from(
|
||||||
@@ -491,10 +417,6 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed Model Training (LoRA) group
|
|
||||||
|
|
||||||
// Removed Performance group (Small Models / Runs on Mac) per request
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface TemplateInfo {
|
|||||||
description: string
|
description: string
|
||||||
localizedTitle?: string
|
localizedTitle?: string
|
||||||
localizedDescription?: string
|
localizedDescription?: string
|
||||||
|
isEssential?: boolean
|
||||||
sourceModule?: string
|
sourceModule?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
models?: string[]
|
models?: string[]
|
||||||
@@ -27,6 +28,8 @@ export interface WorkflowTemplates {
|
|||||||
localizedTitle?: string
|
localizedTitle?: string
|
||||||
category?: string
|
category?: string
|
||||||
type?: string
|
type?: string
|
||||||
|
icon?: string
|
||||||
|
isEssential?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplateGroup {
|
export interface TemplateGroup {
|
||||||
|
|||||||
Reference in New Issue
Block a user