V2 Node Search (+ hidden Node Library changes) (#8987)

## Summary

Redesigned node search with categories

## Changes

- **What**: Adds a v2 search component, leaving the existing
implementation untouched
- It also brings onboard the incomplete node library & preview changes,
disabled and behind a hidden setting
- **Breaking**: Changes the 'default' value of the node search setting
to v2, adding v1 (legacy) as an option

## Screenshots (if applicable)




https://github.com/user-attachments/assets/2ab797df-58f0-48e8-8b20-2a1809e3735f

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8987-V2-Node-Search-hidden-Node-Library-changes-30c6d73d36508160902bcb92553f147c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Yourz <crazilou@vip.qq.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
pythongosssss
2026-02-20 09:10:03 +00:00
committed by GitHub
parent 8f5cdead73
commit 6902e38e6a
183 changed files with 7972 additions and 127 deletions

View File

@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest'
import {
generateCategoryId,
getCategoryIcon,
getProviderBorderStyle,
getProviderIcon
} from './categoryUtil'
describe('getCategoryIcon', () => {
it('returns mapped icon for known category', () => {
expect(getCategoryIcon('all')).toBe('icon-[lucide--list]')
expect(getCategoryIcon('image')).toBe('icon-[lucide--image]')
expect(getCategoryIcon('video')).toBe('icon-[lucide--film]')
})
it('returns folder icon for unknown category', () => {
expect(getCategoryIcon('unknown-category')).toBe('icon-[lucide--folder]')
})
it('is case insensitive', () => {
expect(getCategoryIcon('ALL')).toBe('icon-[lucide--list]')
expect(getCategoryIcon('Image')).toBe('icon-[lucide--image]')
})
})
describe('getProviderIcon', () => {
it('returns icon class for simple provider name', () => {
expect(getProviderIcon('BFL')).toBe('icon-[comfy--bfl]')
expect(getProviderIcon('OpenAI')).toBe('icon-[comfy--openai]')
})
it('converts spaces to hyphens', () => {
expect(getProviderIcon('Stability AI')).toBe('icon-[comfy--stability-ai]')
expect(getProviderIcon('Moonvalley Marey')).toBe(
'icon-[comfy--moonvalley-marey]'
)
})
it('converts to lowercase', () => {
expect(getProviderIcon('GEMINI')).toBe('icon-[comfy--gemini]')
})
})
describe('getProviderBorderStyle', () => {
it('returns solid color for single-color providers', () => {
expect(getProviderBorderStyle('BFL')).toBe('#ffffff')
expect(getProviderBorderStyle('OpenAI')).toBe('#B6B6B6')
expect(getProviderBorderStyle('Bria')).toBe('#B6B6B6')
})
it('returns gradient for dual-color providers', () => {
expect(getProviderBorderStyle('Gemini')).toBe(
'linear-gradient(90deg, #3186FF, #FABC12)'
)
expect(getProviderBorderStyle('Stability AI')).toBe(
'linear-gradient(90deg, #9D39FF, #E80000)'
)
})
it('returns fallback color for unknown providers', () => {
expect(getProviderBorderStyle('Unknown Provider')).toBe('#525252')
})
it('handles provider names with spaces', () => {
expect(getProviderBorderStyle('Stability AI')).toBe(
'linear-gradient(90deg, #9D39FF, #E80000)'
)
expect(getProviderBorderStyle('Moonvalley Marey')).toBe('#DAD9C5')
})
})
describe('generateCategoryId', () => {
it('generates category ID from group and title', () => {
expect(generateCategoryId('Generation', 'Image')).toBe('generation-image')
})
it('converts spaces to hyphens', () => {
expect(generateCategoryId('API Nodes', 'Open Source')).toBe(
'api-nodes-open-source'
)
})
it('converts to lowercase', () => {
expect(generateCategoryId('GENERATION', 'VIDEO')).toBe('generation-video')
})
})

View File

@@ -51,6 +51,79 @@ export const getCategoryIcon = (categoryId: string): string => {
return iconMap[categoryId.toLowerCase()] || 'icon-[lucide--folder]'
}
/**
* Provider brand colors extracted from SVG icons.
* Each entry can be a single color or [color1, color2] for gradient.
*/
const PROVIDER_COLORS: Record<string, string | [string, string]> = {
bfl: '#ffffff',
bria: '#B6B6B6',
bytedance: ['#00C8D2', '#325AB4'],
gemini: ['#3186FF', '#FABC12'],
grok: '#B6B6B6',
hitpaw: '#B6B6B6',
ideogram: '#B6B6B6',
kling: ['#0BF2F9', '#FFF959'],
ltxv: '#B6B6B6',
luma: ['#004EFF', '#00FFFF'],
magnific: ['#EA5A3D', '#F1A64A'],
meshy: ['#67B700', '#FA418C'],
minimax: ['#E2167E', '#FE603C'],
'moonvalley-marey': '#DAD9C5',
openai: '#B6B6B6',
pixverse: ['#B465E6', '#E8632A'],
recraft: '#B6B6B6',
rodin: '#F7F7F7',
runway: '#B6B6B6',
sora: ['#6BB6FE', '#ffffff'],
'stability-ai': ['#9D39FF', '#E80000'],
tencent: ['#004BE5', '#00B3FE'],
topaz: '#B6B6B6',
tripo: ['#F6D85A', '#B6B6B6'],
veo: ['#4285F4', '#EB4335'],
vidu: ['#047FFE', '#40EDD8'],
wan: ['#6156EC', '#F4F3FD'],
wavespeed: '#B6B6B6'
}
/**
* Extracts the provider name from a node category path.
* e.g. "api/image/BFL" -> "BFL"
*/
export function getProviderName(category: string): string {
return category.split('/').at(-1) ?? ''
}
/**
* Returns the icon class for an API node provider (e.g., BFL, OpenAI, Stability AI)
* @param providerName - The provider name from the node category
* @returns The icon class string (e.g., 'icon-[comfy--bfl]')
*/
export function getProviderIcon(providerName: string): string {
const iconKey = providerName.toLowerCase().replaceAll(/\s+/g, '-')
return `icon-[comfy--${iconKey}]`
}
/**
* Returns the border color(s) for an API node provider badge.
* @param providerName - The provider name from the node category
* @returns CSS color string or gradient definition
*/
export function getProviderBorderStyle(providerName: string): string {
const iconKey = providerName.toLowerCase().replaceAll(/\s+/g, '-')
const colors = PROVIDER_COLORS[iconKey]
if (!colors) {
return '#525252' // neutral-600 fallback
}
if (Array.isArray(colors)) {
return `linear-gradient(90deg, ${colors[0]}, ${colors[1]})`
}
return colors
}
/**
* Generates a unique category ID from a category group and title
*/

View File

@@ -1,12 +1,9 @@
import _ from 'es-toolkit/compat'
import type {
ColorOption,
LGraph,
LGraphCanvas
} from '@/lib/litegraph/src/litegraph'
import type { ColorOption, LGraph } from '@/lib/litegraph/src/litegraph'
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
import {
LGraphCanvas,
LGraphGroup,
LGraphNode,
LiteGraph,
@@ -303,6 +300,10 @@ function compressSubgraphWidgetInputSlots(
}
}
export function getLinkTypeColor(typeName: string): string {
return LGraphCanvas.link_type_colors[typeName] ?? LiteGraph.LINK_COLOR
}
export function isLoad3dNode(node: LGraphNode) {
return (
node &&