New searchbox with fuzzy search (WIP) (#83)

* Disable default searchbox

Add headlessui and vue

Add vite's vue plugin

Vue app helloworld

Format vue

Add vue shim

minimal working searchbox

Add primevue dark mode

Use primevue

nit

Add fuse fuzzy search

Add tailwindcss / center searchbox

Fix style

Add node source chip

desc text wrapping

Add placeholder

inputbox filter support wip

Revert some filter designs

Add filter modal

Drop down show all nodes

Change modal font

Add filtered search

nit

Complete on focus

Auto fill filterOption

Fix dropdown

Fix z-index

Fix search bug

Properly remove chip

Adjust node source detection

Resolve merge conflict

nit

* Refactor

* Use badge to display filter type

* nit

* Trigger on canvas event

* nit

* Auto add data type filter when link released

* nit

* Auto focus when shown

* Focus on add/remvoe filter

* close dialog when escape pressed

* Add node at fixed location

* nit

* Update litegraph

* nit

* Change theme

* Increase search limit

* Add node on event location

* Clear filter when dialog closed

* Enable/Disable new search bx

* Improve app loading

* Fix copy node

* Update test expectations

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2024-07-10 19:46:35 -04:00
committed by GitHub
parent 1cb707ea61
commit a28ac0c0fa
57 changed files with 1802 additions and 76 deletions

View File

@@ -0,0 +1,168 @@
import { ComfyNodeDef } from "@/types/apiTypes";
import { getNodeSource } from "@/types/nodeSource";
import Fuse, { IFuseOptions, FuseSearchOptions } from "fuse.js";
import _ from "lodash";
export class FuseSearch<T> {
private fuse: Fuse<T>;
public readonly data: T[];
constructor(
data: T[],
options?: IFuseOptions<T>,
createIndex: boolean = true
) {
this.data = data;
const index =
createIndex && options?.keys
? Fuse.createIndex(options.keys, data)
: undefined;
this.fuse = new Fuse(data, options, index);
}
public search(query: string, options?: FuseSearchOptions): T[] {
if (!query || query === "") {
return [...this.data];
}
return this.fuse.search(query, options).map((result) => result.item);
}
}
export type FilterAndValue<T = string> = [NodeFilter<T>, T];
export abstract class NodeFilter<FilterOptionT = string> {
public abstract readonly id: string;
public abstract readonly name: string;
public abstract readonly invokeSequence: string;
public abstract readonly longInvokeSequence: string;
public readonly fuseSearch: FuseSearch<FilterOptionT>;
constructor(nodeDefs: ComfyNodeDef[], options?: IFuseOptions<FilterOptionT>) {
this.fuseSearch = new FuseSearch(this.getAllNodeOptions(nodeDefs), options);
}
private getAllNodeOptions(nodeDefs: ComfyNodeDef[]): FilterOptionT[] {
return [
...new Set(
nodeDefs.reduce((acc, nodeDef) => {
return [...acc, ...this.getNodeOptions(nodeDef)];
}, [])
),
];
}
public abstract getNodeOptions(node: ComfyNodeDef): FilterOptionT[];
public matches(node: ComfyNodeDef, value: FilterOptionT): boolean {
return this.getNodeOptions(node).includes(value);
}
}
export class InputTypeFilter extends NodeFilter<string> {
public readonly id: string = "input";
public readonly name = "Input Type";
public readonly invokeSequence = "i";
public readonly longInvokeSequence = "input";
public override getNodeOptions(node: ComfyNodeDef): string[] {
const inputs = {
...(node.input.required || {}),
...(node.input.optional || {}),
};
return Object.values(inputs).map((input) => {
const [inputType, inputSpec] = input;
return typeof inputType === "string" ? inputType : "COMBO";
});
}
}
export class OutputTypeFilter extends NodeFilter<string> {
public readonly id: string = "output";
public readonly name = "Output Type";
public readonly invokeSequence = "o";
public readonly longInvokeSequence = "output";
public override getNodeOptions(node: ComfyNodeDef): string[] {
const outputs = node.output;
return outputs.map((output) => {
return typeof output === "string" ? output : output[0];
});
}
}
export class NodeSourceFilter extends NodeFilter<string> {
public readonly id: string = "source";
public readonly name = "Source";
public readonly invokeSequence = "s";
public readonly longInvokeSequence = "source";
public override getNodeOptions(node: ComfyNodeDef): string[] {
return [getNodeSource(node.python_module).displayText];
}
}
export class NodeCategoryFilter extends NodeFilter<string> {
public readonly id: string = "category";
public readonly name = "Category";
public readonly invokeSequence = "c";
public readonly longInvokeSequence = "category";
public override getNodeOptions(node: ComfyNodeDef): string[] {
return [node.category];
}
}
export class NodeSearchService {
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDef>;
public readonly nodeFilters: NodeFilter<string>[];
constructor(data: ComfyNodeDef[]) {
this.nodeFuseSearch = new FuseSearch(data, {
keys: ["name", "display_name", "description"],
includeScore: true,
threshold: 0.6,
shouldSort: true,
});
const filterSearchOptions = {
includeScore: true,
threshold: 0.6,
shouldSort: true,
};
this.nodeFilters = [
new InputTypeFilter(data, filterSearchOptions),
new OutputTypeFilter(data, filterSearchOptions),
new NodeCategoryFilter(data, filterSearchOptions),
];
if (data[0].python_module !== undefined) {
this.nodeFilters.push(new NodeSourceFilter(data, filterSearchOptions));
}
}
public endsWithFilterStartSequence(query: string): boolean {
return query.endsWith(":");
}
public searchNode(
query: string,
filters: FilterAndValue<string>[] = [],
options?: FuseSearchOptions
): ComfyNodeDef[] {
const matchedNodes = this.nodeFuseSearch.search(query);
const results = matchedNodes.filter((node) => {
return _.every(filters, (filterAndValue) => {
const [filter, value] = filterAndValue;
return filter.matches(node, value);
});
});
return options?.limit ? results.slice(0, options.limit) : results;
}
public getFilterById(id: string): NodeFilter<string> | undefined {
return this.nodeFilters.find((filter) => filter.id === id);
}
}