Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0b23783ba | ||
|
|
eeb7310955 | ||
|
|
e40cc5373f | ||
|
|
debdaf418c | ||
|
|
90107ebe1b | ||
|
|
a90c6c1db4 | ||
|
|
1bf3c1dcd0 | ||
|
|
24e95a1954 | ||
|
|
194715920e | ||
|
|
0cb1e92f4f | ||
|
|
d96811a0c3 | ||
|
|
757c587901 | ||
|
|
3b7db4480b | ||
|
|
10bbb393a7 | ||
|
|
c1fc06fd39 | ||
|
|
9de266fbab |
@@ -158,7 +158,7 @@ image = Image.debian_slim()
|
||||
|
||||
target_image = image if deploy_test else dockerfile_image
|
||||
|
||||
@stub.cls(image=target_image, gpu=config["gpu"] ,volumes=volumes, timeout=60 * 10, container_idle_timeout=60 * 5)
|
||||
@stub.cls(image=target_image, gpu=config["gpu"] ,volumes=volumes, timeout=60 * 10, container_idle_timeout=60)
|
||||
class ComfyDeployRunner:
|
||||
|
||||
@enter()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AccessType } from "../../../lib/AccessType";
|
||||
import { MachineList } from "@/components/MachineList";
|
||||
import { SubscriptionProvider } from "@/components/useCurrentPlan";
|
||||
import { db } from "@/db/db";
|
||||
import { machinesTable } from "@/db/schema";
|
||||
import { getCurrentPlanWithAuth } from "@/server/getCurrentPlan";
|
||||
@@ -32,12 +33,12 @@ async function MachineListServer() {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* <div>Machines</div> */}
|
||||
<MachineList
|
||||
sub={sub}
|
||||
data={machines}
|
||||
userMetadata={AccessType.parse(user.privateMetadata ?? {})}
|
||||
/>
|
||||
<SubscriptionProvider sub={sub}>
|
||||
<MachineList
|
||||
data={machines}
|
||||
userMetadata={AccessType.parse(user.privateMetadata ?? {})}
|
||||
/>
|
||||
</SubscriptionProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export const metadata = {
|
||||
title: 'Quickstart',
|
||||
description:
|
||||
'This guide will get you all set up and ready to use the Protocol API. We’ll cover how to get started an API client and how to make your first API request.',
|
||||
'This guide will get you all set up and ready to use Comfy Deploy. We’ll cover how to get started an API client and how to make your first API request.',
|
||||
}
|
||||
|
||||
# Getting stated
|
||||
# Getting started
|
||||
|
||||
Install Comfy Deploy's plugin on your local machine to get started with deploying workflow.
|
||||
Install Comfy Deploy's plugin on your local ComfyUI to get started with deploying workflow.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
export const metadata = {
|
||||
title: 'Comfy Deploy Video tutorials',
|
||||
description:
|
||||
'Get started with API integration to run any deploy ComfyUI workflow',
|
||||
}
|
||||
|
||||
**This is a collection of video tutorials on questions that have come up in our [discord channel](https://discord.gg/Zrar7yP6MJ).**
|
||||
|
||||
# Comfy Deploy workflow walkthrough (comprehensive)
|
||||
|
||||
Local workflow, to production API
|
||||
- Set inputs with Comfy Deploy `External Text` nodes
|
||||
- Deploy the workflow
|
||||
- Create a machine
|
||||
- Deploy an API endpoint for the workflow
|
||||
- Test the workflow in Comfy Deploy
|
||||
|
||||
<video controls>
|
||||
<source src="https://pub-1a2bca0642c24fcfb84ce8d8415958d3.r2.dev/comfydeploy_base-sd-setup.mp4" type="video/mp4"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
|
||||
|
||||
# Test with Docs
|
||||
Shows how you can use the <a href='/docs/endpoints'>endpoints docs</a> to test out the API!
|
||||
|
||||
<video controls>
|
||||
<source src="https://pub-1a2bca0642c24fcfb84ce8d8415958d3.r2.dev/comfydeploy_interactive_docs.mp4" type="video/mp4"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
## Intermediate IPAdaptor example run
|
||||
Shows off a semi-complex workflow working on Comfy Deploy
|
||||
|
||||
<video controls>
|
||||
<source src="https://pub-1a2bca0642c24fcfb84ce8d8415958d3.r2.dev/ipadapters-test.mp4" type="video/mp4"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
## Animate diff on Comfy Deploy
|
||||
Shows off animate diff working on Comfy Deploy
|
||||
|
||||
<video controls>
|
||||
<source src="https://pub-1a2bca0642c24fcfb84ce8d8415958d3.r2.dev/comfydeploy-animediff.mp4" type="video/mp4"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
## Cloning workflows
|
||||
What does cloning a workflow look like
|
||||
|
||||
<video controls>
|
||||
<source src="https://pub-1a2bca0642c24fcfb84ce8d8415958d3.r2.dev/comfydeploy-clone-template.mp4" type="video/mp4"/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
## [Install custom nodes from any github repo (Loom)](https://www.loom.com/share/c2025c5060e348839ade6a3a9d96441d?sid=6a57f779-0209-4341-a5fd-f99769cdb162)
|
||||
- This is only neccessary if the custom node is not in the ComfyUI Manager node list
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
import { getCurrentPlanWithAuth } from "@/server/getCurrentPlan";
|
||||
import * as React from "react";
|
||||
import { createContext } from "react";
|
||||
|
||||
export type CurrentPlanContextType = Awaited<
|
||||
ReturnType<typeof getCurrentPlanWithAuth>
|
||||
>;
|
||||
export const CurrentPlanContext = createContext<
|
||||
CurrentPlanContextType | undefined
|
||||
>(undefined);
|
||||
export function SubscriptionProvider({
|
||||
sub,
|
||||
children,
|
||||
}: {
|
||||
sub: CurrentPlanContextType;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<CurrentPlanContext.Provider value={sub}>
|
||||
{children}
|
||||
</CurrentPlanContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -118,15 +118,12 @@ export function DeploymentDisplay({
|
||||
</TabsList>
|
||||
<TabsContent className="flex flex-col gap-2 !mt-0" value="client">
|
||||
<div>
|
||||
Copy and paste the ComfyDeployClient form
|
||||
<a
|
||||
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
|
||||
className="text-blue-500 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
Install the node comfydeploy SDK
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={`npm i comfydeploy`}
|
||||
/>
|
||||
Initialize your client
|
||||
</div>
|
||||
<CodeBlock
|
||||
lang="js"
|
||||
@@ -200,18 +197,20 @@ function formatCode(
|
||||
if (inputs && inputs.length > 0) {
|
||||
codeTemplate = codeTemplate.replace(
|
||||
"inputs: {}",
|
||||
`inputs: ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
inputs.map((x) => {
|
||||
return [x?.input_id, ""];
|
||||
}),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
.split("\n")
|
||||
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
|
||||
.join("\n")}`,
|
||||
`inputs: ${
|
||||
JSON.stringify(
|
||||
Object.fromEntries(
|
||||
inputs.map((x) => {
|
||||
return [x?.input_id, ""];
|
||||
}),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
.split("\n")
|
||||
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
|
||||
.join("\n")
|
||||
}`,
|
||||
);
|
||||
} else {
|
||||
codeTemplate = codeTemplate.replace(
|
||||
|
||||
@@ -31,6 +31,7 @@ export function InsertModal<
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
title: React.ReactNode;
|
||||
buttonTitle?: React.ReactNode;
|
||||
description: string;
|
||||
dialogClassName?: string;
|
||||
serverAction: (data: z.infer<Z>) => Promise<unknown>;
|
||||
@@ -54,7 +55,7 @@ export function InsertModal<
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
{props.buttonTitle ?? props.title}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
||||
@@ -149,9 +149,16 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
header: () => <div className="text-left">Type</div>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-left font-medium truncate">
|
||||
<Badge
|
||||
className="text-left font-medium truncate"
|
||||
variant={
|
||||
row.original.type == "comfy-deploy-serverless"
|
||||
? "success"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{row.original.type}
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -183,6 +190,7 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
cell: ({ row }) => {
|
||||
const machine = row.original;
|
||||
const [open, setOpen] = useState(false);
|
||||
const sub = useCurrentPlan();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -291,7 +299,10 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
fieldType: "models",
|
||||
},
|
||||
gpu: {
|
||||
inputProps: {},
|
||||
fieldType: "gpuPicker",
|
||||
inputProps: {
|
||||
sub: sub,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -319,14 +330,14 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
},
|
||||
];
|
||||
|
||||
import { useCurrentPlan } from "./useCurrentPlan";
|
||||
|
||||
export function MachineList({
|
||||
data,
|
||||
userMetadata,
|
||||
sub,
|
||||
}: {
|
||||
data: Machine[];
|
||||
userMetadata: z.infer<typeof AccessType>;
|
||||
sub: Awaited<ReturnType<typeof getCurrentPlanWithAuth>>;
|
||||
}) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
@@ -336,6 +347,8 @@ export function MachineList({
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
|
||||
const sub = useCurrentPlan();
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
@@ -390,11 +403,12 @@ export function MachineList({
|
||||
? `Max ${machineMaxCount} ComfyUI machine for your account, upgrade to unlock more cnfiguration.`
|
||||
: `Max ${machineMaxCount} ComfyUI machine for your account`
|
||||
}
|
||||
title={
|
||||
buttonTitle={
|
||||
<>
|
||||
New Machine {locked ? <Lock size={14} /> : <Plus size={14} />}
|
||||
</>
|
||||
}
|
||||
title={"New Machine"}
|
||||
description="Add custom ComfyUI machines to your account."
|
||||
serverAction={addCustomMachine}
|
||||
formSchema={addCustomMachineSchema}
|
||||
@@ -420,11 +434,9 @@ export function MachineList({
|
||||
},
|
||||
},
|
||||
gpu: {
|
||||
fieldType: !userMetadata.betaFeaturesAccess
|
||||
? "fallback"
|
||||
: "select",
|
||||
fieldType: "gpuPicker",
|
||||
inputProps: {
|
||||
disabled: !userMetadata.betaFeaturesAccess,
|
||||
sub: sub,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { getAllUserModels as getAllUserModels } from "@/server/getAllUserModel";
|
||||
import type { getAllUserModels } from "@/server/getAllUserModel";
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
@@ -46,8 +46,10 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
@@ -79,12 +81,10 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
||||
const model = row.original;
|
||||
return (
|
||||
<>
|
||||
{
|
||||
/*<a
|
||||
{/*<a
|
||||
className="hover:underline flex gap-2"
|
||||
href={`/storage/${model.id}`} // TODO
|
||||
>*/
|
||||
}
|
||||
>*/}
|
||||
<span className="truncate max-w-[200px]">
|
||||
{row.original.model_name}
|
||||
</span>
|
||||
@@ -110,9 +110,13 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Badge
|
||||
variant={row.original.status === "failed"
|
||||
? "red"
|
||||
: (row.original.status === "started" ? "yellow" : "green")}
|
||||
variant={
|
||||
row.original.status === "failed"
|
||||
? "red"
|
||||
: row.original.status === "started"
|
||||
? "yellow"
|
||||
: "green"
|
||||
}
|
||||
>
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
@@ -184,10 +188,10 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const model_type_map: Record<modelEnumType, any> = {
|
||||
"checkpoint": "amber",
|
||||
"lora": "green",
|
||||
"embedding": "violet",
|
||||
"vae": "teal",
|
||||
checkpoint: "amber",
|
||||
lora: "green",
|
||||
embedding: "violet",
|
||||
vae: "teal",
|
||||
};
|
||||
|
||||
function getBadgeColor(modelType: modelEnumType) {
|
||||
@@ -257,9 +261,8 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<
|
||||
VisibilityState
|
||||
>({});
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
|
||||
const table = useReactTable({
|
||||
@@ -286,10 +289,12 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
<div className="flex flex-row w-full items-center py-4">
|
||||
<Input
|
||||
placeholder="Filter workflows..."
|
||||
value={(table.getColumn("model_name")?.getFilterValue() as string) ??
|
||||
""}
|
||||
value={
|
||||
(table.getColumn("model_name")?.getFilterValue() as string) ?? ""
|
||||
}
|
||||
onChange={(event) =>
|
||||
table.getColumn("model_name")?.setFilterValue(event.target.value)}
|
||||
table.getColumn("model_name")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<div className="ml-auto flex gap-2">
|
||||
@@ -304,7 +309,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
formSchema={downloadUrlModelSchema}
|
||||
fieldConfig={{
|
||||
url: {
|
||||
fieldType: "fallback",
|
||||
fieldType: "modelUrlPicker",
|
||||
inputProps: { required: true },
|
||||
description: (
|
||||
<>
|
||||
@@ -313,6 +318,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
href="https://www.civitai.com/models"
|
||||
target="_blank"
|
||||
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
|
||||
rel="noreferrer"
|
||||
>
|
||||
civitai.com
|
||||
</a>{" "}
|
||||
@@ -324,10 +330,8 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
fieldType: "select",
|
||||
inputProps: { required: true },
|
||||
description: (
|
||||
<>
|
||||
We'll figure this out if you pick a civitai model
|
||||
</>
|
||||
),
|
||||
<>We'll figure this out if you pick a civitai model</>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -341,10 +345,12 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
@@ -352,34 +358,32 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length
|
||||
? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)
|
||||
: (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
Play,
|
||||
} from "lucide-react";
|
||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import type { z } from "zod";
|
||||
@@ -60,6 +60,7 @@ import { callServerPromise } from "./callServerPromise";
|
||||
import fetcher from "./fetcher";
|
||||
import { ButtonAction } from "@/components/ButtonActionLoader";
|
||||
import { editWorkflowOnMachine } from "@/server/editWorkflowOnMachine";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export function VersionSelect({
|
||||
workflow,
|
||||
@@ -126,12 +127,27 @@ export function MachineSelect({
|
||||
|
||||
export function useSelectedMachine(
|
||||
machines: Awaited<ReturnType<typeof getMachines>>,
|
||||
) {
|
||||
const a = useQueryState("machine", {
|
||||
defaultValue: machines?.[0]?.id ?? "",
|
||||
});
|
||||
): [string, (v: string) => void] {
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
return a;
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
return [
|
||||
searchParams.get("machine") ?? machines?.[0]?.id ?? "",
|
||||
(v: string) => {
|
||||
router.push(pathname + "?" + createQueryString("machine", v));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
type PublicRunStore = {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import * as React from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { z } from "zod";
|
||||
import { CivitalModelSchema, ModelListWrapper } from "./CivitalModelSchema";
|
||||
import { getUrl, mapModelsList } from "./getUrl";
|
||||
import { ModelSelector } from "./ModelSelector";
|
||||
|
||||
export function CivitaiModelRegistry({
|
||||
field,
|
||||
selectMultiple = true,
|
||||
}: Pick<AutoFormInputComponentProps, "field"> & {
|
||||
selectMultiple?: boolean;
|
||||
}) {
|
||||
const [modelList, setModelList] =
|
||||
React.useState<z.infer<typeof ModelListWrapper>>();
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const handleSearch = useDebouncedCallback((search) => {
|
||||
console.log(`Searching... ${search}`);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
fetch(getUrl(search), {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
const list = CivitalModelSchema.parse(a);
|
||||
console.log(a);
|
||||
|
||||
setModelList(mapModelsList(list));
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
setLoading(false);
|
||||
};
|
||||
}, 300);
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(getUrl(), {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
const list = CivitalModelSchema.parse(a);
|
||||
setModelList(mapModelsList(list));
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModelSelector
|
||||
selectMultiple={selectMultiple}
|
||||
field={field}
|
||||
modelList={modelList}
|
||||
label="Civitai"
|
||||
onSearch={handleSearch}
|
||||
shouldFilter={false}
|
||||
isLoading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
import { z } from "zod";
|
||||
|
||||
export const Model = z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
base: z.string(),
|
||||
save_path: z.string(),
|
||||
description: z.string(),
|
||||
reference: z.string(),
|
||||
filename: z.string(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export const CivitalModelSchema = z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
type: z.string(),
|
||||
creator: z
|
||||
.object({
|
||||
username: z.string().nullable(),
|
||||
image: z.string().nullable().default(null),
|
||||
})
|
||||
.nullable(),
|
||||
tags: z.array(z.string()),
|
||||
modelVersions: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
modelId: z.number(),
|
||||
name: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
status: z.string(),
|
||||
publishedAt: z.string(),
|
||||
trainedWords: z.array(z.unknown()),
|
||||
trainingStatus: z.string().nullable(),
|
||||
trainingDetails: z.string().nullable(),
|
||||
baseModel: z.string(),
|
||||
baseModelType: z.string().nullable(),
|
||||
earlyAccessTimeFrame: z.number(),
|
||||
description: z.string().nullable(),
|
||||
vaeId: z.number().nullable(),
|
||||
stats: z.object({
|
||||
downloadCount: z.number(),
|
||||
ratingCount: z.number(),
|
||||
rating: z.number(),
|
||||
}),
|
||||
files: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
sizeKB: z.number(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
downloadUrl: z.string(),
|
||||
}),
|
||||
),
|
||||
images: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
url: z.string(),
|
||||
nsfw: z.string(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
hash: z.string(),
|
||||
type: z.string(),
|
||||
metadata: z.object({
|
||||
hash: z.string(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
}),
|
||||
meta: z.any(),
|
||||
}),
|
||||
),
|
||||
downloadUrl: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
metadata: z.object({
|
||||
totalItems: z.number(),
|
||||
currentPage: z.number(),
|
||||
pageSize: z.number(),
|
||||
totalPages: z.number(),
|
||||
nextPage: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
export const ModelList = z.array(Model);
|
||||
|
||||
export const ModelListWrapper = z.object({
|
||||
models: ModelList,
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import * as React from "react";
|
||||
import { z } from "zod";
|
||||
import { ModelListWrapper } from "./CivitalModelSchema";
|
||||
import { ModelSelector } from "./ModelSelector";
|
||||
|
||||
export function ComfyUIManagerModelRegistry({
|
||||
field,
|
||||
selectMultiple = true,
|
||||
}: Pick<AutoFormInputComponentProps, "field"> & {
|
||||
selectMultiple?: boolean;
|
||||
}) {
|
||||
const [modelList, setModelList] =
|
||||
React.useState<z.infer<typeof ModelListWrapper>>();
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
|
||||
{
|
||||
signal: controller.signal,
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
setModelList(ModelListWrapper.parse(a));
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModelSelector
|
||||
selectMultiple={selectMultiple}
|
||||
field={field}
|
||||
modelList={modelList}
|
||||
label="ComfyUI Manager"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,160 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
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 { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useRef } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { z } from "zod";
|
||||
|
||||
const Model = z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
base: z.string(),
|
||||
save_path: z.string(),
|
||||
description: z.string(),
|
||||
reference: z.string(),
|
||||
filename: z.string(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export const CivitalModelSchema = z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
type: z.string(),
|
||||
// poi: z.boolean(),
|
||||
// nsfw: z.boolean(),
|
||||
// allowNoCredit: z.boolean(),
|
||||
// allowCommercialUse: z.string(),
|
||||
// allowDerivatives: z.boolean(),
|
||||
// allowDifferentLicense: z.boolean(),
|
||||
// stats: z.object({
|
||||
// downloadCount: z.number(),
|
||||
// favoriteCount: z.number(),
|
||||
// commentCount: z.number(),
|
||||
// ratingCount: z.number(),
|
||||
// rating: z.number(),
|
||||
// tippedAmountCount: z.number(),
|
||||
// }),
|
||||
creator: z
|
||||
.object({
|
||||
username: z.string().nullable(),
|
||||
image: z.string().nullable().default(null),
|
||||
})
|
||||
.nullable(),
|
||||
tags: z.array(z.string()),
|
||||
modelVersions: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
modelId: z.number(),
|
||||
name: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
status: z.string(),
|
||||
publishedAt: z.string(),
|
||||
trainedWords: z.array(z.unknown()),
|
||||
trainingStatus: z.string().nullable(),
|
||||
trainingDetails: z.string().nullable(),
|
||||
baseModel: z.string(),
|
||||
baseModelType: z.string().nullable(),
|
||||
earlyAccessTimeFrame: z.number(),
|
||||
description: z.string().nullable(),
|
||||
vaeId: z.number().nullable(),
|
||||
stats: z.object({
|
||||
downloadCount: z.number(),
|
||||
ratingCount: z.number(),
|
||||
rating: z.number(),
|
||||
}),
|
||||
files: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
sizeKB: z.number(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
// metadata: z.object({
|
||||
// fp: z.string().nullable().optional(),
|
||||
// size: z.string().nullable().optional(),
|
||||
// format: z.string().nullable().optional(),
|
||||
// }),
|
||||
// pickleScanResult: z.string(),
|
||||
// pickleScanMessage: z.string(),
|
||||
// virusScanResult: z.string(),
|
||||
// virusScanMessage: z.string().nullable(),
|
||||
// scannedAt: z.string(),
|
||||
// hashes: z.object({
|
||||
// AutoV1: z.string().nullable().optional(),
|
||||
// AutoV2: z.string().nullable().optional(),
|
||||
// SHA256: z.string().nullable().optional(),
|
||||
// CRC32: z.string().nullable().optional(),
|
||||
// BLAKE3: z.string().nullable().optional(),
|
||||
// }),
|
||||
downloadUrl: z.string(),
|
||||
// primary: z.boolean().default(false),
|
||||
})
|
||||
),
|
||||
images: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
url: z.string(),
|
||||
nsfw: z.string(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
hash: z.string(),
|
||||
type: z.string(),
|
||||
metadata: z.object({
|
||||
hash: z.string(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
}),
|
||||
meta: z.any(),
|
||||
})
|
||||
),
|
||||
downloadUrl: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
metadata: z.object({
|
||||
totalItems: z.number(),
|
||||
currentPage: z.number(),
|
||||
pageSize: z.number(),
|
||||
totalPages: z.number(),
|
||||
nextPage: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const ModelList = z.array(Model);
|
||||
|
||||
export const ModelListWrapper = z.object({
|
||||
models: ModelList,
|
||||
});
|
||||
import { CivitaiModelRegistry } from "./CivitaiModelRegistry";
|
||||
import { ComfyUIManagerModelRegistry } from "./ComfyUIManagerModelRegistry";
|
||||
|
||||
export function ModelPickerView({
|
||||
field,
|
||||
@@ -187,240 +44,3 @@ export function ModelPickerView({
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
function mapType(type: string) {
|
||||
switch (type) {
|
||||
case "checkpoint":
|
||||
return "checkpoints";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
function mapModelsList(
|
||||
models: z.infer<typeof CivitalModelSchema>
|
||||
): z.infer<typeof ModelListWrapper> {
|
||||
return {
|
||||
models: models.items.flatMap((item) => {
|
||||
return item.modelVersions.map((v) => {
|
||||
return {
|
||||
name: `${item.name} ${v.name} (${v.files[0].name})`,
|
||||
type: mapType(item.type.toLowerCase()),
|
||||
base: v.baseModel,
|
||||
save_path: "default",
|
||||
description: item.description,
|
||||
reference: "",
|
||||
filename: v.files[0].name,
|
||||
url: v.files[0].downloadUrl,
|
||||
} as z.infer<typeof Model>;
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getUrl(search?: string) {
|
||||
const baseUrl = "https://civitai.com/api/v1/models";
|
||||
const searchParams = {
|
||||
limit: 5,
|
||||
} as any;
|
||||
searchParams["sort"] = "Most Downloaded";
|
||||
|
||||
if (search) {
|
||||
searchParams["query"] = search;
|
||||
} else {
|
||||
// sort: "Highest Rated",
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
Object.keys(searchParams).forEach((key) =>
|
||||
url.searchParams.append(key, searchParams[key])
|
||||
);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function CivitaiModelRegistry({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
const [modelList, setModelList] =
|
||||
React.useState<z.infer<typeof ModelListWrapper>>();
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const handleSearch = useDebouncedCallback((search) => {
|
||||
console.log(`Searching... ${search}`);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
fetch(getUrl(search), {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
const list = CivitalModelSchema.parse(a);
|
||||
console.log(a);
|
||||
|
||||
setModelList(mapModelsList(list));
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
setLoading(false);
|
||||
};
|
||||
}, 300);
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(getUrl(), {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
const list = CivitalModelSchema.parse(a);
|
||||
setModelList(mapModelsList(list));
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModelSelector
|
||||
field={field}
|
||||
modelList={modelList}
|
||||
label="Civitai"
|
||||
onSearch={handleSearch}
|
||||
shouldFilter={false}
|
||||
isLoading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ComfyUIManagerModelRegistry({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
const [modelList, setModelList] =
|
||||
React.useState<z.infer<typeof ModelListWrapper>>();
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
|
||||
{
|
||||
signal: controller.signal,
|
||||
}
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
setModelList(ModelListWrapper.parse(a));
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModelSelector
|
||||
field={field}
|
||||
modelList={modelList}
|
||||
label="ComfyUI Manager"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
field,
|
||||
modelList,
|
||||
label,
|
||||
onSearch,
|
||||
shouldFilter = true,
|
||||
isLoading,
|
||||
}: Pick<AutoFormInputComponentProps, "field"> & {
|
||||
modelList?: z.infer<typeof ModelListWrapper>;
|
||||
label: string;
|
||||
onSearch?: (search: string) => void;
|
||||
shouldFilter?: boolean;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const value = (field.value as z.infer<typeof ModelList>) ?? [];
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
function toggleModel(model: z.infer<typeof Model>) {
|
||||
const prevSelectedModels = value;
|
||||
if (
|
||||
prevSelectedModels.some(
|
||||
(selectedModel) =>
|
||||
selectedModel.url + selectedModel.name === model.url + model.name
|
||||
)
|
||||
) {
|
||||
field.onChange(
|
||||
prevSelectedModels.filter(
|
||||
(selectedModel) =>
|
||||
selectedModel.url + selectedModel.name !== model.url + model.name
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...prevSelectedModels, model]);
|
||||
}
|
||||
}
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="" ref={containerRef}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between flex"
|
||||
>
|
||||
Add from {label}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[375px] p-0" side="bottom">
|
||||
<Command shouldFilter={shouldFilter}>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
className="h-9"
|
||||
onValueChange={onSearch}
|
||||
>
|
||||
{isLoading && <LoadingIcon />}
|
||||
</CommandInput>
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
<CommandList className="pointer-events-auto">
|
||||
<CommandGroup>
|
||||
{modelList?.models.map((model) => (
|
||||
<CommandItem
|
||||
key={model.url + model.name}
|
||||
value={model.url}
|
||||
onSelect={() => {
|
||||
toggleModel(model);
|
||||
}}
|
||||
>
|
||||
{model.name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.some(
|
||||
(selectedModel) => selectedModel.url === model.url
|
||||
)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import { ModelListWrapper, Model, ModelList } from "./CivitalModelSchema";
|
||||
|
||||
export function ModelSelector({
|
||||
field,
|
||||
modelList,
|
||||
label,
|
||||
onSearch,
|
||||
shouldFilter = true,
|
||||
isLoading,
|
||||
selectMultiple = true,
|
||||
}: Pick<AutoFormInputComponentProps, "field"> & {
|
||||
modelList?: z.infer<typeof ModelListWrapper>;
|
||||
label: string;
|
||||
onSearch?: (search: string) => void;
|
||||
shouldFilter?: boolean;
|
||||
isLoading?: boolean;
|
||||
selectMultiple?: boolean;
|
||||
}) {
|
||||
const value = (field.value as z.infer<typeof ModelList>) ?? [];
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
function toggleModel(model: z.infer<typeof Model>) {
|
||||
const prevSelectedModels = value;
|
||||
if (
|
||||
prevSelectedModels.some(
|
||||
(selectedModel) =>
|
||||
selectedModel.url + selectedModel.name === model.url + model.name,
|
||||
)
|
||||
) {
|
||||
field.onChange(
|
||||
prevSelectedModels.filter(
|
||||
(selectedModel) =>
|
||||
selectedModel.url + selectedModel.name !== model.url + model.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (!selectMultiple) {
|
||||
field.onChange([model]);
|
||||
} else {
|
||||
field.onChange([...prevSelectedModels, model]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="" ref={containerRef}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between flex"
|
||||
>
|
||||
Add from {label}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[375px] p-0" side="bottom">
|
||||
<Command shouldFilter={shouldFilter}>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
className="h-9"
|
||||
onValueChange={onSearch}
|
||||
>
|
||||
{isLoading && <LoadingIcon />}
|
||||
</CommandInput>
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
<CommandList className="pointer-events-auto">
|
||||
<CommandGroup>
|
||||
{modelList?.models.map((model) => (
|
||||
<CommandItem
|
||||
key={model.url + model.name}
|
||||
value={model.url}
|
||||
onSelect={() => {
|
||||
toggleModel(model);
|
||||
}}
|
||||
>
|
||||
{model.name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.some(
|
||||
(selectedModel) => selectedModel.url === model.url,
|
||||
)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -17,6 +19,22 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -25,7 +43,15 @@ import {
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { findAllDeployments } from "@/server/curdDeploments";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import {
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
FolderInput,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@@ -39,19 +65,95 @@ export function SnapshotPickerView({
|
||||
<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} />
|
||||
<div className="flex flex-wrap gap-2 justify-end">
|
||||
<CustomNodesSelector field={field} />
|
||||
<SnapshotPresetPicker field={field} />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" className="w-fit">
|
||||
Edit <Edit size={14}></Edit>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px] h-full max-h-[600px] grid grid-rows-[auto,1fr,auto]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit custom nodes</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make advacne changes to the snapshots
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
className="h-full p-2 max-h-[600px] 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));
|
||||
}}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
<Button type="button" variant="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
{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>
|
||||
<div className="flex gap-2 flex-col">
|
||||
{Object.entries(field.value.git_custom_nodes).map(
|
||||
([key, item]: [string, any], index) => (
|
||||
<Card className="p-4 flex gap-4 items-center justify-between">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="bg-gray-200 aspect-square w-6 h-6 rounded-full text-center">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
href={key}
|
||||
className="hover:underline flex items-center gap-2"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLink size={12} /> {key}
|
||||
</a>
|
||||
<div className="text-2xs text-primary/50">
|
||||
{item.hash}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild type="button">
|
||||
<Button type="button" variant={"ghost"}>
|
||||
<MoreVertical size={12} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
disabled={key.endsWith("comfyui-deploy.git")}
|
||||
// className="opacity-50"
|
||||
onClick={() => {
|
||||
const newNodeList = {
|
||||
...field.value.git_custom_nodes,
|
||||
};
|
||||
delete newNodeList[key];
|
||||
const nodeList = newNodeList;
|
||||
const newValue = {
|
||||
...field.value,
|
||||
git_custom_nodes: nodeList,
|
||||
};
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@@ -65,13 +167,14 @@ function SnapshotPresetPicker({
|
||||
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;
|
||||
}[]
|
||||
>();
|
||||
const [frameworks, setFramework] =
|
||||
React.useState<
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}[]
|
||||
>();
|
||||
|
||||
React.useEffect(() => {
|
||||
findAllDeployments().then((a) => {
|
||||
@@ -108,12 +211,13 @@ function SnapshotPresetPicker({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between flex"
|
||||
className="w-fit justify-between flex"
|
||||
>
|
||||
{selected
|
||||
<FolderInput size={14} />
|
||||
Import
|
||||
{/* {selected
|
||||
? findItem(selected)?.label
|
||||
: "Select snapshot (From deployments)"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
: "Select snapshot (From deployments)"} */}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[375px] p-0">
|
||||
@@ -140,7 +244,7 @@ function SnapshotPresetPicker({
|
||||
"ml-auto h-4 w-4",
|
||||
field.value === framework.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
@@ -199,12 +303,12 @@ function CustomNodesSelector({
|
||||
|
||||
const { data, error, isLoading } = useSWR<CustomNodeList>(
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json",
|
||||
fetcher
|
||||
fetcher,
|
||||
);
|
||||
|
||||
const keys = React.useMemo(
|
||||
() => Object.keys(customNodeList),
|
||||
[customNodeList, data]
|
||||
[customNodeList, data],
|
||||
);
|
||||
|
||||
function findItem(value: string) {
|
||||
@@ -213,6 +317,11 @@ function CustomNodesSelector({
|
||||
return included;
|
||||
}
|
||||
|
||||
const onChangeRef = React.useRef(field.onChange);
|
||||
React.useEffect(() => {
|
||||
onChangeRef.current = field.onChange;
|
||||
}, [field.onChange]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -220,10 +329,10 @@ function CustomNodesSelector({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between flex"
|
||||
className="w-fit justify-between flex"
|
||||
>
|
||||
Add custom nodes - {keys.length} selected
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<Plus size={14}></Plus> <Badge>{keys.length} </Badge>
|
||||
{/* <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> */}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[375px] p-0" side="bottom">
|
||||
@@ -243,7 +352,7 @@ function CustomNodesSelector({
|
||||
{
|
||||
hash: string;
|
||||
disabled: boolean;
|
||||
pip?: string[]
|
||||
pip?: string[];
|
||||
}
|
||||
>;
|
||||
const x = customNodeList;
|
||||
@@ -256,7 +365,7 @@ function CustomNodesSelector({
|
||||
const repoName = extractRepoName(currentValue);
|
||||
const id = toast.loading(`Fetching repo info...`);
|
||||
const repo = await fetch(
|
||||
`https://api.github.com/repos/${repoName}`
|
||||
`https://api.github.com/repos/${repoName}`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
@@ -267,14 +376,16 @@ function CustomNodesSelector({
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error(`Failed to fetch repo info ${e.message}`);
|
||||
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}`
|
||||
`https://api.github.com/repos/${repoName}/branches/${branch}`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => BranchInfoSchema.parse(x))
|
||||
@@ -282,7 +393,7 @@ function CustomNodesSelector({
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error(
|
||||
`Failed to fetch branch info ${e.message}`
|
||||
`Failed to fetch branch info ${e.message}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
@@ -291,19 +402,27 @@ function CustomNodesSelector({
|
||||
|
||||
if (!branchInfo) return;
|
||||
|
||||
const value: Record<string, any> = {
|
||||
hash: branchInfo?.commit.sha,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (framework.pip) {
|
||||
value["pip"] = framework.pip;
|
||||
}
|
||||
|
||||
nodeList = {
|
||||
[currentValue]: {
|
||||
hash: branchInfo?.commit.sha,
|
||||
disabled: false,
|
||||
pip: framework.pip
|
||||
},
|
||||
...x,
|
||||
[currentValue]: value,
|
||||
};
|
||||
}
|
||||
field.onChange({
|
||||
|
||||
const newValue = {
|
||||
...field.value,
|
||||
git_custom_nodes: nodeList,
|
||||
});
|
||||
};
|
||||
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
>
|
||||
{framework.title}
|
||||
@@ -312,7 +431,7 @@ function CustomNodesSelector({
|
||||
"ml-auto h-4 w-4",
|
||||
findItem(framework.reference)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CivitalModelSchema,
|
||||
ModelListWrapper,
|
||||
Model,
|
||||
} from "./CivitalModelSchema";
|
||||
|
||||
function mapType(type: string) {
|
||||
switch (type) {
|
||||
case "checkpoint":
|
||||
return "checkpoints";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
export function mapModelsList(
|
||||
models: z.infer<typeof CivitalModelSchema>,
|
||||
): z.infer<typeof ModelListWrapper> {
|
||||
return {
|
||||
models: models.items.flatMap((item) => {
|
||||
return item.modelVersions.map((v) => {
|
||||
return {
|
||||
name: `${item.name} ${v.name} (${v.files[0].name})`,
|
||||
type: mapType(item.type.toLowerCase()),
|
||||
base: v.baseModel,
|
||||
save_path: "default",
|
||||
description: item.description,
|
||||
reference: "",
|
||||
filename: v.files[0].name,
|
||||
// Quick hack to get the download url back as normal url
|
||||
url: `https://civitai.com/models/${v.modelId}?modelVersionId=${v.id}`, //v.files[0].downloadUrl,
|
||||
} as z.infer<typeof Model>;
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
export function getUrl(search?: string) {
|
||||
const baseUrl = "https://civitai.com/api/v1/models";
|
||||
const searchParams = {
|
||||
limit: 5,
|
||||
} as any;
|
||||
searchParams["sort"] = "Most Downloaded";
|
||||
|
||||
if (search) {
|
||||
searchParams["query"] = search;
|
||||
} else {
|
||||
// sort: "Highest Rated",
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
Object.keys(searchParams).forEach((key) =>
|
||||
url.searchParams.append(key, searchParams[key]),
|
||||
);
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { AutoFormInputComponentProps } from "@/components/ui/auto-form/types";
|
||||
import { getBaseSchema } from "@/components/ui/auto-form/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Lock } from "lucide-react";
|
||||
import * as z from "zod";
|
||||
|
||||
export default function AutoFormGPUPicker({
|
||||
label,
|
||||
isRequired,
|
||||
field,
|
||||
fieldConfigItem,
|
||||
zodItem,
|
||||
}: AutoFormInputComponentProps) {
|
||||
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def
|
||||
.values;
|
||||
|
||||
let values: [string, string][] = [];
|
||||
if (!Array.isArray(baseValues)) {
|
||||
values = Object.entries(baseValues);
|
||||
} else {
|
||||
values = baseValues.map((value) => [value, value]);
|
||||
}
|
||||
|
||||
function findItem(value: any) {
|
||||
return values.find((item) => item[0] === value);
|
||||
}
|
||||
|
||||
const plan = fieldConfigItem.inputProps?.sub?.plan;
|
||||
const enabledGPU = ["T4"];
|
||||
|
||||
const planMapping: Record<string, string> = {
|
||||
A10G: "pro",
|
||||
A100: "enterprise",
|
||||
};
|
||||
|
||||
if (plan == "pro") {
|
||||
enabledGPU.push("A10G");
|
||||
} else if (plan == "enterprise") {
|
||||
enabledGPU.push("A10G");
|
||||
enabledGPU.push("A100");
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{label}
|
||||
{isRequired && <span className="text-destructive"> *</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
className="w-full"
|
||||
placeholder={fieldConfigItem.inputProps?.placeholder}
|
||||
>
|
||||
{field.value ? findItem(field.value)?.[1] : "Select an option"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{values.map(([value, label]) => {
|
||||
const enabled = enabledGPU.includes(value);
|
||||
return (
|
||||
<SelectItem value={label} key={value} disabled={!enabled}>
|
||||
{label}
|
||||
{!enabled && (
|
||||
<span className="mx-2 inline-flex items-center justify-center gap-2">
|
||||
<Badge className="capitalize">{planMapping[value]}</Badge>{" "}
|
||||
plan required
|
||||
<Lock size={14}></Lock>
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{fieldConfigItem.description && (
|
||||
<FormDescription>{fieldConfigItem.description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "../ui/form";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import * as React from "react";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { CivitaiModelRegistry } from "./CivitaiModelRegistry";
|
||||
import { ComfyUIManagerModelRegistry } from "./ComfyUIManagerModelRegistry";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ModelList } from "@/components/custom-form/CivitalModelSchema";
|
||||
import { z } from "zod";
|
||||
|
||||
export default function AutoFormModelsPickerUrl({
|
||||
label,
|
||||
isRequired,
|
||||
field,
|
||||
fieldConfigItem,
|
||||
zodItem,
|
||||
fieldProps,
|
||||
}: AutoFormInputComponentProps) {
|
||||
return (
|
||||
<FormItem>
|
||||
{fieldConfigItem.inputProps?.showLabel && (
|
||||
<FormLabel>
|
||||
{label}
|
||||
{isRequired && <span className="text-destructive"> *</span>}
|
||||
</FormLabel>
|
||||
)}
|
||||
<FormControl>
|
||||
<Suspense fallback={<LoadingIcon />}>
|
||||
<ModelPickerView field={field} fieldProps={fieldProps} />
|
||||
</Suspense>
|
||||
</FormControl>
|
||||
{fieldConfigItem.description && (
|
||||
<FormDescription>{fieldConfigItem.description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelPickerView({
|
||||
field,
|
||||
fieldProps,
|
||||
}: Pick<AutoFormInputComponentProps, "field" | "fieldProps">) {
|
||||
const customOverride = React.useMemo(() => {
|
||||
const customOnChange = (value: z.infer<typeof ModelList>) => {
|
||||
const model = value[0];
|
||||
field.onChange(model?.url);
|
||||
};
|
||||
return {
|
||||
...field,
|
||||
onChange: customOnChange,
|
||||
value: field.value
|
||||
? [
|
||||
{
|
||||
url: field.value,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}, [field]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 flex-col px-1">
|
||||
<ComfyUIManagerModelRegistry
|
||||
field={customOverride}
|
||||
selectMultiple={false}
|
||||
/>
|
||||
<CivitaiModelRegistry field={customOverride} selectMultiple={false} />
|
||||
<Input
|
||||
// className="min-h-[150px] max-h-[300px] p-2 rounded-lg text-xs w-full"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import {
|
||||
FormControl,
|
||||
@@ -7,10 +9,19 @@ import {
|
||||
FormMessage,
|
||||
} from "../ui/form";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
import { ModelPickerView } from "@/components/custom-form/ModelPickerView";
|
||||
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import * as React from "react";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ComfyUIManagerModelRegistry } from "./ComfyUIManagerModelRegistry";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
export default function AutoFormModelsPicker({
|
||||
label,
|
||||
@@ -27,6 +38,7 @@ export default function AutoFormModelsPicker({
|
||||
{isRequired && <span className="text-destructive"> *</span>}
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
<FormControl>
|
||||
<Suspense fallback={<LoadingIcon />}>
|
||||
<ModelPickerView field={field} />
|
||||
@@ -35,7 +47,54 @@ export default function AutoFormModelsPicker({
|
||||
{fieldConfigItem.description && (
|
||||
<FormDescription>{fieldConfigItem.description}</FormDescription>
|
||||
)}
|
||||
<FormDescription>
|
||||
{" "}
|
||||
<div className="text-sm">
|
||||
Models are moving to{" "}
|
||||
<a
|
||||
href="/storage"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 underline"
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
Storage
|
||||
</a>
|
||||
</div>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelPickerView({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
return (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-sm">
|
||||
Models (ComfyUI Manager)
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex gap-2 flex-col px-1">
|
||||
<ComfyUIManagerModelRegistry field={field} />
|
||||
{/* <CivitaiModelRegistry field={field} /> */}
|
||||
{/* <span>{field.value.length} selected</span> */}
|
||||
{field.value && (
|
||||
<ScrollArea className="w-full bg-gray-100 mx-auto rounded-lg mt-2">
|
||||
<Textarea
|
||||
className="min-h-[150px] max-h-[300px] p-2 rounded-lg text-xs w-full"
|
||||
value={JSON.stringify(field.value, null, 2)}
|
||||
onChange={(e) => {
|
||||
field.onChange(JSON.parse(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,6 +242,10 @@ export const navigation: Array<NavGroup> = [
|
||||
title: "API",
|
||||
links: [{ title: "Endpoints", href: "/docs/endpoints" }],
|
||||
},
|
||||
{
|
||||
title: "Video Tutorials",
|
||||
links: [{ title: "Archive", href: "/docs/videos" }],
|
||||
},
|
||||
];
|
||||
|
||||
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AutoFormGPUPicker from "@/components/custom-form/gpu-picker";
|
||||
import AutoFormCheckbox from "./fields/checkbox";
|
||||
import AutoFormDate from "./fields/date";
|
||||
import AutoFormEnum from "./fields/enum";
|
||||
@@ -8,6 +9,7 @@ import AutoFormSwitch from "./fields/switch";
|
||||
import AutoFormTextarea from "./fields/textarea";
|
||||
import AutoFormModelsPicker from "@/components/custom-form/model-picker";
|
||||
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
|
||||
import AutoFormModelsPickerUrl from "@/components/custom-form/model-picker-url-only";
|
||||
|
||||
export const INPUT_COMPONENTS = {
|
||||
checkbox: AutoFormCheckbox,
|
||||
@@ -22,6 +24,8 @@ export const INPUT_COMPONENTS = {
|
||||
// Customs
|
||||
snapshot: AutoFormSnapshotPicker,
|
||||
models: AutoFormModelsPicker,
|
||||
gpuPicker: AutoFormGPUPicker,
|
||||
modelUrlPicker: AutoFormModelsPickerUrl,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { getCurrentPlanWithAuth } from "@/server/getCurrentPlan";
|
||||
import type { INPUT_COMPONENTS } from "./config";
|
||||
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
|
||||
import type * as z from "zod";
|
||||
@@ -6,6 +7,7 @@ export type FieldConfigItem = {
|
||||
description?: React.ReactNode;
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
showLabel?: boolean;
|
||||
sub?: Awaited<ReturnType<typeof getCurrentPlanWithAuth>>;
|
||||
};
|
||||
fieldType?:
|
||||
| keyof typeof INPUT_COMPONENTS
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { getCurrentPlanWithAuth } from "@/server/getCurrentPlan";
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
type CurrentPlanContextType = Awaited<
|
||||
ReturnType<typeof getCurrentPlanWithAuth>
|
||||
>;
|
||||
const CurrentPlanContext = createContext<CurrentPlanContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function SubscriptionProvider({
|
||||
sub,
|
||||
children,
|
||||
}: {
|
||||
sub: CurrentPlanContextType;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<CurrentPlanContext.Provider value={sub}>
|
||||
{children}
|
||||
</CurrentPlanContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useCurrentPlan = (): CurrentPlanContextType => {
|
||||
const context = useContext(CurrentPlanContext);
|
||||
|
||||
// if (context === undefined) {
|
||||
// throw new Error("useCurrentPlan must be used within a CurrentPlanProvider");
|
||||
// }
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export const insertCustomMachineSchema = createInsertSchema(machinesTable, {
|
||||
comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
||||
git_custom_nodes: {
|
||||
"https://github.com/BennyKok/comfyui-deploy.git": {
|
||||
hash: "43fe0a384aa5fa9e141d4a264b2ed40a73b817bc",
|
||||
hash: "a838cb7ad425e5652c3931fbafdc886b53c48a22",
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ import { and, desc, eq, isNull, or } from "drizzle-orm";
|
||||
import { subscriptionStatusTable } from "@/db/schema";
|
||||
import { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import "server-only";
|
||||
|
||||
export async function getCurrentPlanWithAuth() {
|
||||
const { userId, orgId } = auth();
|
||||
@@ -23,7 +24,10 @@ export async function getCurrentPlan({ user_id, org_id }: APIKeyUserType) {
|
||||
eq(subscriptionStatusTable.user_id, user_id),
|
||||
org_id
|
||||
? eq(subscriptionStatusTable.org_id, org_id)
|
||||
: or(isNull(subscriptionStatusTable.org_id), eq(subscriptionStatusTable.org_id, "")),
|
||||
: or(
|
||||
isNull(subscriptionStatusTable.org_id),
|
||||
eq(subscriptionStatusTable.org_id, ""),
|
||||
),
|
||||
),
|
||||
orderBy: desc(subscriptionStatusTable.created_at),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user