mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 11:42:06 +00:00
feat: implement fuzzy search for widgets and nodes using Fuse in Right Side Panel (#8043)
related https://github.com/Comfy-Org/ComfyUI_frontend/pull/7812#discussion_r2685117810 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8043-feat-implement-fuzzy-search-for-widgets-and-nodes-using-Fuse-in-Right-Side-Panel-2e86d73d365081d7869cfa1956dfc0ad) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -38,10 +38,22 @@ describe('searchWidgets', () => {
|
|||||||
|
|
||||||
expect(searchWidgets(widgets, 'width')).toHaveLength(1)
|
expect(searchWidgets(widgets, 'width')).toHaveLength(1)
|
||||||
expect(searchWidgets(widgets, 'slider')).toHaveLength(1)
|
expect(searchWidgets(widgets, 'slider')).toHaveLength(1)
|
||||||
expect(searchWidgets(widgets, 'high')).toHaveLength(1)
|
|
||||||
expect(searchWidgets(widgets, 'image')).toHaveLength(1)
|
expect(searchWidgets(widgets, 'image')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support fuzzy matching (e.g., "high" matches both "height" and value "high")', () => {
|
||||||
|
const widgets = [
|
||||||
|
createWidget('width', 'number', '100', 'Size Control'),
|
||||||
|
createWidget('height', 'slider', '200', 'Image Height'),
|
||||||
|
createWidget('quality', 'text', 'high', 'Quality')
|
||||||
|
]
|
||||||
|
|
||||||
|
const results = searchWidgets(widgets, 'high')
|
||||||
|
expect(results).toHaveLength(2)
|
||||||
|
expect(results.some((r) => r.widget.name === 'height')).toBe(true)
|
||||||
|
expect(results.some((r) => r.widget.name === 'quality')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('should handle multiple search words', () => {
|
it('should handle multiple search words', () => {
|
||||||
const widgets = [
|
const widgets = [
|
||||||
createWidget('width', 'number', '100', 'Image Width'),
|
createWidget('width', 'number', '100', 'Image Width'),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { InjectionKey, MaybeRefOrGetter } from 'vue'
|
import type { InjectionKey, MaybeRefOrGetter } from 'vue'
|
||||||
import { computed, toValue } from 'vue'
|
import { computed, toValue } from 'vue'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import type { IFuseOptions } from 'fuse.js'
|
||||||
|
|
||||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||||
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -18,10 +20,18 @@ export type NodeWidgetsListList = Array<{
|
|||||||
widgets: NodeWidgetsList
|
widgets: NodeWidgetsList
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
interface WidgetSearchItem {
|
||||||
|
index: number
|
||||||
|
searchableLabel: string
|
||||||
|
searchableName: string
|
||||||
|
searchableType: string
|
||||||
|
searchableValue: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches widgets in a list and returns search results.
|
* Searches widgets in a list using fuzzy search and returns search results.
|
||||||
|
* Uses Fuse.js for better matching with typo tolerance and relevance ranking.
|
||||||
* Filters by name, localized label, type, and user-input value.
|
* Filters by name, localized label, type, and user-input value.
|
||||||
* Performs basic tokenization of the query string.
|
|
||||||
*/
|
*/
|
||||||
export function searchWidgets<T extends { widget: IBaseWidget }[]>(
|
export function searchWidgets<T extends { widget: IBaseWidget }[]>(
|
||||||
list: T,
|
list: T,
|
||||||
@@ -30,27 +40,48 @@ export function searchWidgets<T extends { widget: IBaseWidget }[]>(
|
|||||||
if (query.trim() === '') {
|
if (query.trim() === '') {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
const words = query.trim().toLowerCase().split(' ')
|
|
||||||
return list.filter(({ widget }) => {
|
const searchableList: WidgetSearchItem[] = list.map((item, index) => {
|
||||||
const label = widget.label?.toLowerCase()
|
const searchableItem = {
|
||||||
const name = widget.name.toLowerCase()
|
index,
|
||||||
const type = widget.type.toLowerCase()
|
searchableLabel: item.widget.label?.toLowerCase() || '',
|
||||||
const value = widget.value?.toString().toLowerCase()
|
searchableName: item.widget.name.toLowerCase(),
|
||||||
return words.every(
|
searchableType: item.widget.type.toLowerCase(),
|
||||||
(word) =>
|
searchableValue: item.widget.value?.toString().toLowerCase() || ''
|
||||||
name.includes(word) ||
|
}
|
||||||
label?.includes(word) ||
|
return searchableItem
|
||||||
type?.includes(word) ||
|
})
|
||||||
value?.includes(word)
|
|
||||||
|
const fuseOptions: IFuseOptions<WidgetSearchItem> = {
|
||||||
|
keys: [
|
||||||
|
{ name: 'searchableName', weight: 0.4 },
|
||||||
|
{ name: 'searchableLabel', weight: 0.3 },
|
||||||
|
{ name: 'searchableValue', weight: 0.3 },
|
||||||
|
{ name: 'searchableType', weight: 0.2 }
|
||||||
|
],
|
||||||
|
threshold: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuse = new Fuse(searchableList, fuseOptions)
|
||||||
|
const results = fuse.search(query.trim())
|
||||||
|
|
||||||
|
const matchedItems = new Set(
|
||||||
|
results.map((result) => list[result.item.index]!)
|
||||||
)
|
)
|
||||||
}) as T
|
|
||||||
|
return list.filter((item) => matchedItems.has(item)) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeSearchItem = {
|
||||||
|
nodeId: NodeId
|
||||||
|
searchableTitle: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches widgets and nodes in a list and returns search results.
|
* Searches widgets and nodes in a list using fuzzy search and returns search results.
|
||||||
|
* Uses Fuse.js for node title matching with typo tolerance and relevance ranking.
|
||||||
* First checks if the node title matches the query (if so, keeps entire node).
|
* First checks if the node title matches the query (if so, keeps entire node).
|
||||||
* Otherwise, filters widgets using searchWidgets.
|
* Otherwise, filters widgets using searchWidgets.
|
||||||
* Performs basic tokenization of the query string.
|
|
||||||
*/
|
*/
|
||||||
export function searchWidgetsAndNodes(
|
export function searchWidgetsAndNodes(
|
||||||
list: NodeWidgetsListList,
|
list: NodeWidgetsListList,
|
||||||
@@ -59,12 +90,26 @@ export function searchWidgetsAndNodes(
|
|||||||
if (query.trim() === '') {
|
if (query.trim() === '') {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
const words = query.trim().toLowerCase().split(' ')
|
|
||||||
|
const searchableList: NodeSearchItem[] = list.map((item) => ({
|
||||||
|
nodeId: item.node.id,
|
||||||
|
searchableTitle: (item.node.getTitle() ?? '').toLowerCase()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const fuseOptions: IFuseOptions<NodeSearchItem> = {
|
||||||
|
keys: [{ name: 'searchableTitle', weight: 1.0 }],
|
||||||
|
threshold: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuse = new Fuse(searchableList, fuseOptions)
|
||||||
|
const nodeMatches = fuse.search(query.trim())
|
||||||
|
const matchedNodeIds = new Set(
|
||||||
|
nodeMatches.map((result) => result.item.nodeId)
|
||||||
|
)
|
||||||
|
|
||||||
return list
|
return list
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const { node } = item
|
if (matchedNodeIds.has(item.node.id)) {
|
||||||
const title = node.getTitle().toLowerCase()
|
|
||||||
if (words.every((word) => title.includes(word))) {
|
|
||||||
return { ...item, keep: true }
|
return { ...item, keep: true }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user