mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-29 08:47:31 +00:00
Compare commits
1 Commits
test/stand
...
feat/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
853782e395 |
@@ -20,9 +20,12 @@
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<i class="icon-[comfy--node] size-4 shrink-0 text-muted-foreground" />
|
||||
<span class="text-foreground min-w-0 flex-1 truncate text-sm">
|
||||
<span
|
||||
class="text-foreground min-w-0 flex-1 truncate text-sm [&_.highlight]:rounded-sm [&_.highlight]:bg-primary-background [&_.highlight]:px-0.5 [&_.highlight]:font-bold [&_.highlight]:text-white"
|
||||
>
|
||||
<slot name="node" :node="item.value">
|
||||
{{ item.value.label }}
|
||||
<span v-if="highlightedLabel" v-html="highlightedLabel" />
|
||||
<template v-else>{{ item.value.label }}</template>
|
||||
</slot>
|
||||
</span>
|
||||
<button
|
||||
@@ -90,13 +93,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { FlattenedItem } from 'reka-ui'
|
||||
import { TreeItem } from 'reka-ui'
|
||||
import { computed, inject } from 'vue'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
|
||||
import NodePreviewCard from '@/components/node/NodePreviewCard.vue'
|
||||
import { useNodePreviewAndDrag } from '@/composables/node/useNodePreviewAndDrag'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { InjectKeyContextMenuNode } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
InjectKeyContextMenuNode,
|
||||
InjectKeySearchHighlights
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -119,9 +125,18 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const contextMenuNode = inject(InjectKeyContextMenuNode)
|
||||
const searchHighlights = inject(
|
||||
InjectKeySearchHighlights,
|
||||
ref(new Map<string, string>())
|
||||
)
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
|
||||
const nodeDef = computed(() => item.value.data)
|
||||
const highlightedLabel = computed(() => {
|
||||
const name = nodeDef.value?.name
|
||||
if (!name) return ''
|
||||
return searchHighlights.value.get(name) ?? ''
|
||||
})
|
||||
|
||||
const isBookmarked = computed(() => {
|
||||
if (!nodeDef.value) return false
|
||||
|
||||
@@ -164,7 +164,7 @@ import {
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger
|
||||
} from 'reka-ui'
|
||||
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
||||
import { computed, nextTick, onMounted, provide, ref, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
@@ -200,6 +200,7 @@ import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import { InjectKeySearchHighlights } from '@/types/treeExplorerTypes'
|
||||
|
||||
import AllNodesPanel from './nodeLibrary/AllNodesPanel.vue'
|
||||
import BlueprintsPanel from './nodeLibrary/BlueprintsPanel.vue'
|
||||
@@ -261,18 +262,74 @@ const expandedKeys = usePerTabState(selectedTab, expandedKeysByTab)
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const { startDrag } = useNodeDragToCanvas()
|
||||
|
||||
const filteredNodeDefs = computed(() => {
|
||||
if (searchQuery.value.length === 0) {
|
||||
return []
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
function buildHighlightHtml(
|
||||
text: string,
|
||||
indices: ReadonlyArray<readonly [number, number]>
|
||||
): string {
|
||||
if (indices.length === 0) return escapeHtml(text)
|
||||
|
||||
const sorted = [...indices].sort((a, b) => a[0] - b[0])
|
||||
let result = ''
|
||||
let lastIndex = 0
|
||||
for (const [start, end] of sorted) {
|
||||
if (start > lastIndex) {
|
||||
result += escapeHtml(text.slice(lastIndex, start))
|
||||
}
|
||||
result += `<span class="highlight">${escapeHtml(text.slice(start, end + 1))}</span>`
|
||||
lastIndex = end + 1
|
||||
}
|
||||
return nodeDefStore.nodeSearchService.searchNode(
|
||||
searchQuery.value,
|
||||
[],
|
||||
{ limit: 64 },
|
||||
{ matchWildcards: false }
|
||||
)
|
||||
if (lastIndex < text.length) {
|
||||
result += escapeHtml(text.slice(lastIndex))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const searchResult = computed(() => {
|
||||
if (searchQuery.value.length === 0) {
|
||||
return {
|
||||
items: [] as ComfyNodeDefImpl[],
|
||||
highlights: new Map<string, string>()
|
||||
}
|
||||
}
|
||||
|
||||
const { items, matchesByNode } =
|
||||
nodeDefStore.nodeSearchService.searchNodeWithMatches(
|
||||
searchQuery.value,
|
||||
[],
|
||||
{ limit: 64 },
|
||||
{ matchWildcards: false }
|
||||
)
|
||||
|
||||
const highlights = new Map<string, string>()
|
||||
for (const [nodeName, matches] of matchesByNode) {
|
||||
const displayNameMatch = matches.find((m) => m.key === 'display_name')
|
||||
if (displayNameMatch) {
|
||||
const node = items.find((n) => n.name === nodeName)
|
||||
if (node) {
|
||||
highlights.set(
|
||||
nodeName,
|
||||
buildHighlightHtml(node.display_name, displayNameMatch.indices)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { items, highlights }
|
||||
})
|
||||
|
||||
const filteredNodeDefs = computed(() => searchResult.value.items)
|
||||
const searchHighlights = computed(() => searchResult.value.highlights)
|
||||
provide(InjectKeySearchHighlights, searchHighlights)
|
||||
|
||||
const activeNodes = computed(() =>
|
||||
filteredNodeDefs.value.length > 0
|
||||
? filteredNodeDefs.value
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { FuseSearchOptions } from 'fuse.js'
|
||||
import type { FuseResultMatch, FuseSearchOptions } from 'fuse.js'
|
||||
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
import { FuseFilter, FuseSearch } from '@/utils/fuseUtil'
|
||||
|
||||
interface NodeSearchResult {
|
||||
items: ComfyNodeDefImpl[]
|
||||
matchesByNode: Map<string, readonly FuseResultMatch[]>
|
||||
}
|
||||
|
||||
export class NodeSearchService {
|
||||
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
|
||||
public readonly inputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
@@ -99,6 +104,43 @@ export class NodeSearchService {
|
||||
return options?.limit ? results.slice(0, options.limit) : results
|
||||
}
|
||||
|
||||
public searchNodeWithMatches(
|
||||
query: string,
|
||||
filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[] = [],
|
||||
options?: FuseSearchOptions,
|
||||
extraOptions: { matchWildcards?: boolean } = {}
|
||||
): NodeSearchResult {
|
||||
const { matchWildcards = true } = extraOptions
|
||||
const wildcard = matchWildcards ? '*' : undefined
|
||||
const matchedResults = this.nodeFuseSearch.searchWithMatches(query)
|
||||
|
||||
const matchesByNode = new Map<string, readonly FuseResultMatch[]>()
|
||||
for (const r of matchedResults) {
|
||||
matchesByNode.set(r.item.name, r.matches)
|
||||
}
|
||||
|
||||
const matchedNodes = matchedResults.map((r) => r.item)
|
||||
const results = matchedNodes.filter((node) =>
|
||||
filters.every(({ filterDef, value }) => filterDef.matches(node, value))
|
||||
)
|
||||
|
||||
if (matchWildcards) {
|
||||
const alreadyValid = new Set(results.map((r) => r.name))
|
||||
results.push(
|
||||
...matchedNodes
|
||||
.filter((node) => !alreadyValid.has(node.name))
|
||||
.filter((node) =>
|
||||
filters.every(({ filterDef, value }) =>
|
||||
filterDef.matches(node, value, { wildcard })
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const items = options?.limit ? results.slice(0, options.limit) : results
|
||||
return { items, matchesByNode }
|
||||
}
|
||||
|
||||
get nodeFilters(): FuseFilter<ComfyNodeDefImpl, string>[] {
|
||||
return [
|
||||
this.inputTypeFilter,
|
||||
|
||||
@@ -99,3 +99,6 @@ export const InjectKeyExpandedKeys: InjectionKey<
|
||||
export const InjectKeyContextMenuNode: InjectionKey<
|
||||
Ref<RenderedTreeExplorerNode<ComfyNodeDefImpl> | null>
|
||||
> = Symbol()
|
||||
|
||||
export const InjectKeySearchHighlights: InjectionKey<Ref<Map<string, string>>> =
|
||||
Symbol()
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import type { FuseOptionKey, FuseSearchOptions, IFuseOptions } from 'fuse.js'
|
||||
import type {
|
||||
FuseOptionKey,
|
||||
FuseResult,
|
||||
FuseResultMatch,
|
||||
FuseSearchOptions,
|
||||
IFuseOptions
|
||||
} from 'fuse.js'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
export type SearchAuxScore = number[]
|
||||
|
||||
interface FuseSearchResultWithMatches<T> {
|
||||
item: T
|
||||
matches: readonly FuseResultMatch[]
|
||||
}
|
||||
|
||||
export interface FuseFilterWithValue<T, O = string> {
|
||||
filterDef: FuseFilter<T, O>
|
||||
value: O
|
||||
@@ -109,7 +120,7 @@ export class FuseSearch<T> {
|
||||
createIndex && this.keys.length
|
||||
? Fuse.createIndex(this.keys, data)
|
||||
: undefined
|
||||
this.fuse = new Fuse(data, fuseOptions, index)
|
||||
this.fuse = new Fuse(data, { ...fuseOptions, includeMatches: true }, index)
|
||||
}
|
||||
|
||||
public search(query: string, options?: FuseSearchOptions): T[] {
|
||||
@@ -135,6 +146,38 @@ export class FuseSearch<T> {
|
||||
return aux.map((x) => x.item)
|
||||
}
|
||||
|
||||
public searchWithMatches(
|
||||
query: string,
|
||||
options?: FuseSearchOptions
|
||||
): FuseSearchResultWithMatches<T>[] {
|
||||
if (!query) {
|
||||
return this.data.map((item) => ({ item, matches: [] }))
|
||||
}
|
||||
|
||||
const fuseResult: FuseResult<T>[] = this.fuse.search(query, options)
|
||||
|
||||
const toResult = (r: FuseResult<T>): FuseSearchResultWithMatches<T> => ({
|
||||
item: r.item,
|
||||
matches: r.matches ?? []
|
||||
})
|
||||
|
||||
if (!this.advancedScoring) {
|
||||
return fuseResult.map(toResult)
|
||||
}
|
||||
|
||||
return fuseResult
|
||||
.map((r) => ({
|
||||
result: toResult(r),
|
||||
scores: this.calcAuxScores(
|
||||
query.toLocaleLowerCase(),
|
||||
r.item,
|
||||
r.score ?? 0
|
||||
)
|
||||
}))
|
||||
.sort((a, b) => this.compareAux(a.scores, b.scores))
|
||||
.map((x) => x.result)
|
||||
}
|
||||
|
||||
public calcAuxScores(query: string, entry: T, score: number): SearchAuxScore {
|
||||
let values: string[] = []
|
||||
if (typeof entry === 'string') {
|
||||
|
||||
Reference in New Issue
Block a user