feat: add drop down selection for storage
This commit is contained in:
parent
3b7db4480b
commit
757c587901
@ -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>
|
||||
|
72
web/src/components/custom-form/CivitaiModelRegistry.tsx
Normal file
72
web/src/components/custom-form/CivitaiModelRegistry.tsx
Normal file
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
94
web/src/components/custom-form/CivitalModelSchema.tsx
Normal file
94
web/src/components/custom-form/CivitalModelSchema.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
|
123
web/src/components/custom-form/ModelSelector.tsx
Normal file
123
web/src/components/custom-form/ModelSelector.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
55
web/src/components/custom-form/getUrl.tsx
Normal file
55
web/src/components/custom-form/getUrl.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
"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,
|
||||
url: 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;
|
||||
}
|
96
web/src/components/custom-form/model-picker-url-only.tsx
Normal file
96
web/src/components/custom-form/model-picker-url-only.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
"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>) => {
|
||||
field.onChange(value[0]?.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 { CivitaiModelRegistry } from "./CivitaiModelRegistry";
|
||||
import { ComfyUIManagerModelRegistry } from "./ComfyUIManagerModelRegistry";
|
||||
|
||||
export default function AutoFormModelsPicker({
|
||||
label,
|
||||
@ -39,3 +50,35 @@ export default function AutoFormModelsPicker({
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelPickerView({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
return (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-sm">
|
||||
Models (ComfyUI Manager & Civitai)
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -9,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,
|
||||
@ -24,6 +25,7 @@ export const INPUT_COMPONENTS = {
|
||||
snapshot: AutoFormSnapshotPicker,
|
||||
models: AutoFormModelsPicker,
|
||||
gpuPicker: AutoFormGPUPicker,
|
||||
modelUrlPicker: AutoFormModelsPickerUrl,
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user