comfyui-deploy/web/src/components/custom-form/SnapshotPickerView.tsx

327 lines
10 KiB
TypeScript

"use client";
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import fetcher from "@/components/fetcher";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { findAllDeployments } from "@/server/curdDeploments";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { toast } from "sonner";
import useSWR from "swr";
import { z } from "zod";
export function SnapshotPickerView({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
return (
<Accordion type="single" collapsible>
<AccordionItem value="item-1">
<AccordionTrigger className="text-sm">Custom Nodes</AccordionTrigger>
<AccordionContent className="flex gap-2 flex-col px-1">
<SnapshotPresetPicker field={field} />
<CustomNodesSelector field={field} />
{field.value && (
// <ScrollArea className="w-full bg-gray-100 mx-auto max-w-[500px] rounded-lg">
<Textarea
className="min-h-[150px] max-h-[300px] p-2 rounded-md text-xs w-full"
value={JSON.stringify(field.value, null, 2)}
onChange={(e) => {
// Update field.onChange to pass the array of selected models
field.onChange(JSON.parse(e.target.value));
}}
/>
// </ScrollArea>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
function SnapshotPresetPicker({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<string | null>(null);
const [frameworks, setFramework] = React.useState<
{
id: string;
label: string;
value: string;
}[]
>();
React.useEffect(() => {
findAllDeployments().then((a) => {
// console.log(a);
const frameworks = a
.map((item) => {
if (
item.deployments.length == 0 ||
item.deployments[0].version.snapshot == null
)
return null;
return {
id: item.deployments[0].version.id,
label: `${item.name} - ${item.deployments[0].environment}`,
value: JSON.stringify(item.deployments[0].version.snapshot),
};
})
.filter((item): item is NonNullable<typeof item> => item != null);
setFramework(frameworks);
});
}, []);
function findItem(value: string) {
return frameworks?.find((item) => item.id === value);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between flex"
>
{selected
? findItem(selected)?.label
: "Select snapshot (From deployments)"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[375px] p-0">
<Command>
<CommandInput placeholder="Search snapshot..." className="h-9" />
<CommandEmpty>No snapshot found.</CommandEmpty>
<CommandGroup>
{frameworks?.map((framework) => (
<CommandItem
key={framework.id}
value={framework.id}
onSelect={(currentValue) => {
setSelected(currentValue);
const json =
frameworks?.find((item) => item.id === currentValue)
?.value ?? null;
field.onChange(json ? JSON.parse(json) : null);
setOpen(false);
}}
>
{framework.label}
<Check
className={cn(
"ml-auto h-4 w-4",
field.value === framework.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}
type CustomNodeList = {
custom_nodes: {
author: string;
title: string;
reference: string;
pip: string[];
files: string[];
install_type: string;
description: string;
}[];
};
const RepoSchema = z.object({
default_branch: z.string(),
});
const BranchInfoSchema = z.object({
commit: z.object({
sha: z.string(),
}),
});
function extractRepoName(repoUrl: string) {
const url = new URL(repoUrl);
const pathParts = url.pathname.split("/");
const repoName = pathParts[2].replace(".git", "");
const author = pathParts[1];
return `${author}/${repoName}`;
}
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"
>
Add 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={async (currentValue) => {
let nodeList: Record<
string,
{
hash: string;
disabled: boolean;
pip?: string[]
}
>;
const x = customNodeList;
if (x[currentValue]) {
const newNodeList = { ...x };
delete newNodeList[currentValue];
nodeList = newNodeList;
} else {
const repoName = extractRepoName(currentValue);
const id = toast.loading(`Fetching repo info...`);
const repo = await fetch(
`https://api.github.com/repos/${repoName}`
)
.then((x) => x.json())
.then((x) => {
console.log(x);
return x;
})
.then((x) => RepoSchema.parse(x))
.catch((e) => {
console.error(e);
toast.dismiss(id);
toast.error(`Failed to fetch repo info ${e.message}`);
return null;
});
if (!repo) return;
const branch = repo.default_branch;
const branchInfo = await fetch(
`https://api.github.com/repos/${repoName}/branches/${branch}`
)
.then((x) => x.json())
.then((x) => BranchInfoSchema.parse(x))
.catch((e) => {
console.error(e);
toast.dismiss(id);
toast.error(
`Failed to fetch branch info ${e.message}`
);
return null;
});
toast.dismiss(id);
if (!branchInfo) return;
nodeList = {
[currentValue]: {
hash: branchInfo?.commit.sha,
disabled: false,
pip: framework.pip
},
...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>
);
}