feat: add civital model install
This commit is contained in:
parent
1f91d4d357
commit
1d25aadd74
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
@ -95,6 +95,7 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unist-util-filter": "^5.0.1",
|
"unist-util-filter": "^5.0.1",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
|
"use-debounce": "^10.0.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||||
|
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@ -21,6 +22,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Check, ChevronsUpDown } from "lucide-react";
|
import { Check, ChevronsUpDown } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const Model = z.object({
|
const Model = z.object({
|
||||||
@ -34,6 +36,114 @@ const Model = z.object({
|
|||||||
url: 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);
|
const ModelList = z.array(Model);
|
||||||
|
|
||||||
export const ModelListWrapper = z.object({
|
export const ModelListWrapper = z.object({
|
||||||
@ -43,16 +153,132 @@ export const ModelListWrapper = z.object({
|
|||||||
export function ModelPickerView({
|
export function ModelPickerView({
|
||||||
field,
|
field,
|
||||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||||
const value = (field.value as z.infer<typeof ModelList>) ?? [];
|
return (
|
||||||
|
<div className="flex gap-2 flex-col">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
function mapModelsList(
|
||||||
|
models: z.infer<typeof CivitalModelSchema>
|
||||||
|
): z.infer<typeof ModelListWrapper> {
|
||||||
|
return {
|
||||||
|
models: models.items.map((item) => {
|
||||||
|
const v = item.modelVersions[0];
|
||||||
|
return {
|
||||||
|
name: `${item.name} ${v.name} (${v.files[0].name})`,
|
||||||
|
type: v.files[0].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] =
|
const [modelList, setModelList] =
|
||||||
React.useState<z.infer<typeof ModelListWrapper>>();
|
React.useState<z.infer<typeof ModelListWrapper>>();
|
||||||
|
|
||||||
// const [selectedModels, setSelectedModels] = React.useState<
|
const [loading, setLoading] = React.useState(false);
|
||||||
// z.infer<typeof ModelList>
|
|
||||||
// >(field.value ?? []);
|
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(() => {
|
React.useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -72,6 +298,26 @@ export function ModelPickerView({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
return <ModelSelector field={field} modelList={modelList} label="common" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
function toggleModel(model: z.infer<typeof Model>) {
|
||||||
const prevSelectedModels = value;
|
const prevSelectedModels = value;
|
||||||
if (
|
if (
|
||||||
@ -91,10 +337,6 @@ export function ModelPickerView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// field.onChange(selectedModels);
|
|
||||||
// }, [selectedModels]);
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,13 +349,19 @@ export function ModelPickerView({
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-full justify-between flex"
|
className="w-full justify-between flex"
|
||||||
>
|
>
|
||||||
Select models... ({value.length} selected)
|
Select {label}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[375px] p-0" side="top">
|
<PopoverContent className="w-[375px] p-0" side="bottom">
|
||||||
<Command>
|
<Command shouldFilter={shouldFilter}>
|
||||||
<CommandInput placeholder="Search models..." className="h-9" />
|
<CommandInput
|
||||||
|
placeholder="Search models..."
|
||||||
|
className="h-9"
|
||||||
|
onValueChange={onSearch}
|
||||||
|
>
|
||||||
|
{isLoading && <LoadingIcon />}
|
||||||
|
</CommandInput>
|
||||||
<CommandEmpty>No framework found.</CommandEmpty>
|
<CommandEmpty>No framework found.</CommandEmpty>
|
||||||
<CommandList className="pointer-events-auto">
|
<CommandList className="pointer-events-auto">
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
@ -123,7 +371,6 @@ export function ModelPickerView({
|
|||||||
value={model.url}
|
value={model.url}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
toggleModel(model);
|
toggleModel(model);
|
||||||
// Update field.onChange to pass the array of selected models
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{model.name}
|
{model.name}
|
||||||
@ -144,23 +391,6 @@ export function ModelPickerView({
|
|||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
{field.value && (
|
|
||||||
<ScrollArea className="w-full bg-gray-100 mx-auto rounded-lg mt-2">
|
|
||||||
{/* <div className="max-h-[200px]">
|
|
||||||
<pre className="p-2 rounded-md text-xs ">
|
|
||||||
{JSON.stringify(field.value, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div> */}
|
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ export default function AutoFormObject<
|
|||||||
const { shape } = getBaseSchema<SchemaType>(schema);
|
const { shape } = getBaseSchema<SchemaType>(schema);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion type="multiple" className="space-y-5">
|
<Accordion type="multiple" className="space-y-5 py-1">
|
||||||
{Object.keys(shape).map((name) => {
|
{Object.keys(shape).map((name) => {
|
||||||
const item = shape[name] as z.ZodAny;
|
const item = shape[name] as z.ZodAny;
|
||||||
const zodBaseType = getBaseType(item);
|
const zodBaseType = getBaseType(item);
|
||||||
|
@ -70,7 +70,7 @@ function AutoForm<SchemaType extends ZodObjectOrWrapped>({
|
|||||||
className={cn("space-y-5", className)}
|
className={cn("space-y-5", className)}
|
||||||
>
|
>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="max-h-[400px] px-1 py-1 w-full">
|
<div className="max-h-[400px] px-1 w-full">
|
||||||
<AutoFormObject
|
<AutoFormObject
|
||||||
schema={objectFormSchema}
|
schema={objectFormSchema}
|
||||||
form={form}
|
form={form}
|
||||||
|
@ -39,7 +39,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
const CommandInput = React.forwardRef<
|
const CommandInput = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
@ -50,6 +50,7 @@ const CommandInput = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user