Compare commits

..
16 Commits
26 changed files with 1078 additions and 532 deletions
+1 -1
View File
@@ -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()
+7 -6
View File
@@ -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>
);
}
+3 -3
View File
@@ -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. Well 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. Well 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>
+58
View File
@@ -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>
);
}
+20 -21
View File
@@ -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&nbsp;
<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(
+2 -1
View File
@@ -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>
+22 -10
View File
@@ -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,
},
},
}}
+60 -56
View File
@@ -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>
+22 -6
View File
@@ -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>
+56
View File
@@ -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>
);
}
+4
View File
@@ -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,
};
/**
+2
View File
@@ -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
+36
View File
@@ -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;
};
+1 -1
View File
@@ -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,
},
},
+5 -1
View File
@@ -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),
});