feat: add custom nodes selectors

This commit is contained in:
BennyKok 2024-01-11 21:27:56 +08:00
parent 94fa92ed76
commit 6e3b7f0435
8 changed files with 202 additions and 70 deletions

Binary file not shown.

View File

@ -47,6 +47,7 @@
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@tanstack/react-table": "^8.10.7", "@tanstack/react-table": "^8.10.7",
"@tanstack/react-virtual": "beta",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"@types/react-highlight-words": "^0.16.7", "@types/react-highlight-words": "^0.16.7",
"@types/swagger-ui-react": "^4.18.3", "@types/swagger-ui-react": "^4.18.3",

View File

@ -17,6 +17,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import * as React from "react"; import * as React from "react";
import { useState } from "react"; import { useState } from "react";
import type { UnknownKeysParam, ZodObject, ZodRawShape, z } from "zod"; import type { UnknownKeysParam, ZodObject, ZodRawShape, z } from "zod";
@ -30,6 +31,7 @@ export function InsertModal<
disabled?: boolean; disabled?: boolean;
title: string; title: string;
description: string; description: string;
dialogClassName?: string;
serverAction: (data: z.infer<Z>) => Promise<unknown>; serverAction: (data: z.infer<Z>) => Promise<unknown>;
formSchema: Z; formSchema: Z;
fieldConfig?: FieldConfig<z.infer<Z>>; fieldConfig?: FieldConfig<z.infer<Z>>;
@ -70,7 +72,7 @@ export function InsertModal<
</Button> </Button>
)} )}
{/* </DialogTrigger> */} {/* </DialogTrigger> */}
<DialogContent className="sm:max-w-[425px]"> <DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
<DialogHeader> <DialogHeader>
<DialogTitle>{props.title}</DialogTitle> <DialogTitle>{props.title}</DialogTitle>
<DialogDescription>{props.description}</DialogDescription> <DialogDescription>{props.description}</DialogDescription>
@ -108,6 +110,7 @@ export function UpdateModal<
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
title: string; title: string;
description: string; description: string;
dialogClassName?: string;
data: z.infer<Z>; data: z.infer<Z>;
serverAction: ( serverAction: (
data: z.infer<Z> & { data: z.infer<Z> & {
@ -130,7 +133,7 @@ export function UpdateModal<
{/* <DialogTrigger asChild> {/* <DialogTrigger asChild>
<DropdownMenuItem>{props.title}</DropdownMenuItem> <DropdownMenuItem>{props.title}</DropdownMenuItem>
</DialogTrigger> */} </DialogTrigger> */}
<DialogContent className="sm:max-w-[425px]"> <DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
<DialogHeader> <DialogHeader>
<DialogTitle>{props.title}</DialogTitle> <DialogTitle>{props.title}</DialogTitle>
<DialogDescription>{props.description}</DialogDescription> <DialogDescription>{props.description}</DialogDescription>

View File

@ -214,6 +214,7 @@ export const columns: ColumnDef<Machine>[] = [
</DropdownMenuContent> </DropdownMenuContent>
{machine.type === "comfy-deploy-serverless" ? ( {machine.type === "comfy-deploy-serverless" ? (
<UpdateModal <UpdateModal
dialogClassName="sm:max-w-[600px]"
data={machine} data={machine}
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
@ -303,6 +304,7 @@ export function MachineList({ data }: { data: Machine[] }) {
/> />
<div className="ml-auto flex gap-2"> <div className="ml-auto flex gap-2">
<InsertModal <InsertModal
dialogClassName="sm:max-w-[600px]"
disabled={data.some( disabled={data.some(
(machine) => machine.type === "comfy-deploy-serverless" (machine) => machine.type === "comfy-deploy-serverless"
)} )}

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { callServerPromise } from "./callServerPromise"; import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { LoadingIcon } from "@/components/LoadingIcon"; import { LoadingIcon } from "@/components/LoadingIcon";
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form"; import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@ -361,14 +362,6 @@ export function getWorkflowVersionFromVersionIndex(
return workflow_version; return workflow_version;
} }
export default async function fetcher<JSON = unknown>(
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
const res = await fetch(input, init);
return res.json();
}
export function ViewWorkflowDetailsButton({ export function ViewWorkflowDetailsButton({
workflow, workflow,
}: { }: {

View File

@ -112,7 +112,7 @@ export function ModelPickerView({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[375px] p-0" side="top"> <PopoverContent className="w-[375px] p-0" side="top">
<Command> <Command>
<CommandInput placeholder="Search framework..." className="h-9" /> <CommandInput placeholder="Search models..." className="h-9" />
<CommandEmpty>No framework found.</CommandEmpty> <CommandEmpty>No framework found.</CommandEmpty>
<CommandList className="pointer-events-auto"> <CommandList className="pointer-events-auto">
<CommandGroup> <CommandGroup>
@ -144,7 +144,7 @@ export function ModelPickerView({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{field.value && ( {field.value && (
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[360px] rounded-lg mt-2"> <ScrollArea className="w-full bg-gray-100 mx-auto max-w-[500px] rounded-lg mt-2">
<div className="max-h-[200px]"> <div className="max-h-[200px]">
<pre className="p-2 rounded-md text-xs "> <pre className="p-2 rounded-md text-xs ">
{JSON.stringify(field.value, null, 2)} {JSON.stringify(field.value, null, 2)}

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import type { AutoFormInputComponentProps } from "../ui/auto-form/types"; import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import fetcher from "@/components/fetcher";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Command, Command,
@ -8,6 +9,7 @@ import {
CommandGroup, CommandGroup,
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { import {
Popover, Popover,
@ -19,9 +21,30 @@ import { cn } from "@/lib/utils";
import { findAllDeployments } from "@/server/curdDeploments"; import { findAllDeployments } from "@/server/curdDeploments";
import { Check, ChevronsUpDown } from "lucide-react"; import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react"; import * as React from "react";
import useSWR from "swr";
export function SnapshotPickerView({ export function SnapshotPickerView({
field, field,
}: Pick<AutoFormInputComponentProps, "field">) {
return (
<div className="flex gap-2 flex-col">
<SnapshotPresetPicker field={field} />
<CustomNodesSelector field={field} />
{field.value && (
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[500px] rounded-lg">
<div className="max-h-[200px] w-full">
<pre className="p-2 rounded-md text-xs w-full">
{JSON.stringify(field.value, null, 2)}
</pre>
</div>
</ScrollArea>
)}
</div>
);
}
function SnapshotPresetPicker({
field,
}: Pick<AutoFormInputComponentProps, "field">) { }: Pick<AutoFormInputComponentProps, "field">) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<string | null>(null); const [selected, setSelected] = React.useState<string | null>(null);
@ -59,13 +82,10 @@ export function SnapshotPickerView({
}, []); }, []);
function findItem(value: string) { function findItem(value: string) {
// console.log(frameworks);
return frameworks?.find((item) => item.id === value); return frameworks?.find((item) => item.id === value);
} }
return ( return (
<div className="">
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -111,15 +131,119 @@ export function SnapshotPickerView({
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{field.value && ( );
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[360px] rounded-lg mt-2"> }
<div className="max-h-[200px]">
<pre className="p-2 rounded-md text-xs "> type CustomNodeList = {
{JSON.stringify(field.value, null, 2)} custom_nodes: {
</pre> author: string;
</div> title: string;
</ScrollArea> reference: string;
)} files: string[];
</div> install_type: string;
description: string;
}[];
};
function CustomNodesSelector({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
const [open, setOpen] = React.useState(false);
const customNodeList =
field.value.git_custom_nodes ??
({} as Record<
string,
{
hash: string;
disabled: boolean;
}
>);
const { data, error, isLoading } = useSWR<CustomNodeList>(
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json",
fetcher
);
const keys = React.useMemo(
() => Object.keys(customNodeList),
[customNodeList, data]
);
function findItem(value: string) {
// console.log(keys, value.toLowerCase());
const included = keys.includes(value.toLowerCase());
return included;
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between flex"
>
Select custom nodes... {keys.length} selected
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[375px] p-0" side="bottom">
<Command>
<CommandInput placeholder="Search custom nodes..." className="h-9" />
<CommandEmpty>No custom nodes found.</CommandEmpty>
<CommandList>
<CommandGroup>
{data &&
data.custom_nodes?.map((framework, index) => (
<CommandItem
key={index}
value={framework.reference}
onSelect={(currentValue) => {
let nodeList: Record<
string,
{
hash: string;
disabled: boolean;
}
>;
const x = customNodeList;
if (x[currentValue]) {
const newNodeList = { ...x };
delete newNodeList[currentValue];
nodeList = newNodeList;
} else {
nodeList = {
[currentValue]: {
hash: "latest",
disabled: false,
},
...x,
};
}
field.onChange({
...field.value,
git_custom_nodes: nodeList,
});
}}
>
{framework.title}
<Check
className={cn(
"ml-auto h-4 w-4",
findItem(framework.reference)
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
); );
} }

View File

@ -0,0 +1,9 @@
"use client";
export default async function fetcher<JSON = unknown>(
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
const res = await fetch(input, init);
return res.json();
}