diff --git a/web/src/components/ModelList.tsx b/web/src/components/ModelList.tsx index ec4e8cd..26b0db2 100644 --- a/web/src/components/ModelList.tsx +++ b/web/src/components/ModelList.tsx @@ -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[] = [ id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> @@ -79,12 +81,10 @@ export const columns: ColumnDef[] = [ const model = row.original; return ( <> - { - /**/ - } + >*/} {row.original.model_name} @@ -110,9 +110,13 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { return ( {row.original.status} @@ -184,10 +188,10 @@ export const columns: ColumnDef[] = [ }, cell: ({ row }) => { const model_type_map: Record = { - "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( [], ); - const [columnVisibility, setColumnVisibility] = React.useState< - VisibilityState - >({}); + const [columnVisibility, setColumnVisibility] = + React.useState({}); const [rowSelection, setRowSelection] = React.useState({}); const table = useReactTable({ @@ -286,10 +289,12 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
- table.getColumn("model_name")?.setFilterValue(event.target.value)} + table.getColumn("model_name")?.setFilterValue(event.target.value) + } className="max-w-sm" />
@@ -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 {" "} @@ -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 ( - {header.isPlaceholder ? null : flexRender( - header.column.columnDef.header, - header.getContext(), - )} + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} ); })} @@ -352,34 +358,32 @@ export function ModelList({ data }: { data: ModelItemList[] }) { ))} - {table.getRowModel().rows?.length - ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) - : ( - - - No results. - + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} - )} + )) + ) : ( + + + No results. + + + )} diff --git a/web/src/components/custom-form/CivitaiModelRegistry.tsx b/web/src/components/custom-form/CivitaiModelRegistry.tsx new file mode 100644 index 0000000..6d6aa8c --- /dev/null +++ b/web/src/components/custom-form/CivitaiModelRegistry.tsx @@ -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 & { + selectMultiple?: boolean; +}) { + const [modelList, setModelList] = + React.useState>(); + + 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 ( + + ); +} diff --git a/web/src/components/custom-form/CivitalModelSchema.tsx b/web/src/components/custom-form/CivitalModelSchema.tsx new file mode 100644 index 0000000..b6c15ed --- /dev/null +++ b/web/src/components/custom-form/CivitalModelSchema.tsx @@ -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, +}); diff --git a/web/src/components/custom-form/ComfyUIManagerModelRegistry.tsx b/web/src/components/custom-form/ComfyUIManagerModelRegistry.tsx new file mode 100644 index 0000000..0ff3013 --- /dev/null +++ b/web/src/components/custom-form/ComfyUIManagerModelRegistry.tsx @@ -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 & { + selectMultiple?: boolean; +}) { + const [modelList, setModelList] = + React.useState>(); + + 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 ( + + ); +} diff --git a/web/src/components/custom-form/ModelPickerView.tsx b/web/src/components/custom-form/ModelPickerView.tsx index b4fca11..eff107a 100644 --- a/web/src/components/custom-form/ModelPickerView.tsx +++ b/web/src/components/custom-form/ModelPickerView.tsx @@ -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({ ); } - -function mapType(type: string) { - switch (type) { - case "checkpoint": - return "checkpoints"; - } - return type; -} - -function mapModelsList( - models: z.infer -): z.infer { - 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; - }); - }), - }; -} - -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) { - const [modelList, setModelList] = - React.useState>(); - - 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 ( - - ); -} - -export function ComfyUIManagerModelRegistry({ - field, -}: Pick) { - const [modelList, setModelList] = - React.useState>(); - - 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 ( - - ); -} - -export function ModelSelector({ - field, - modelList, - label, - onSearch, - shouldFilter = true, - isLoading, -}: Pick & { - modelList?: z.infer; - label: string; - onSearch?: (search: string) => void; - shouldFilter?: boolean; - isLoading?: boolean; -}) { - const value = (field.value as z.infer) ?? []; - const [open, setOpen] = React.useState(false); - - function toggleModel(model: z.infer) { - 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(null); - - return ( -
- - - - - - - - {isLoading && } - - No models found. - - - {modelList?.models.map((model) => ( - { - toggleModel(model); - }} - > - {model.name} - selectedModel.url === model.url - ) - ? "opacity-100" - : "opacity-0" - )} - /> - - ))} - - - - - -
- ); -} diff --git a/web/src/components/custom-form/ModelSelector.tsx b/web/src/components/custom-form/ModelSelector.tsx new file mode 100644 index 0000000..6043fb7 --- /dev/null +++ b/web/src/components/custom-form/ModelSelector.tsx @@ -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 & { + modelList?: z.infer; + label: string; + onSearch?: (search: string) => void; + shouldFilter?: boolean; + isLoading?: boolean; + selectMultiple?: boolean; +}) { + const value = (field.value as z.infer) ?? []; + const [open, setOpen] = React.useState(false); + + function toggleModel(model: z.infer) { + 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(null); + + return ( +
+ + + + + + + + {isLoading && } + + No models found. + + + {modelList?.models.map((model) => ( + { + toggleModel(model); + }} + > + {model.name} + selectedModel.url === model.url, + ) + ? "opacity-100" + : "opacity-0", + )} + /> + + ))} + + + + + +
+ ); +} diff --git a/web/src/components/custom-form/getUrl.tsx b/web/src/components/custom-form/getUrl.tsx new file mode 100644 index 0000000..3d0081e --- /dev/null +++ b/web/src/components/custom-form/getUrl.tsx @@ -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, +): z.infer { + 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; + }); + }), + }; +} +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; +} diff --git a/web/src/components/custom-form/model-picker-url-only.tsx b/web/src/components/custom-form/model-picker-url-only.tsx new file mode 100644 index 0000000..136f429 --- /dev/null +++ b/web/src/components/custom-form/model-picker-url-only.tsx @@ -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 ( + + {fieldConfigItem.inputProps?.showLabel && ( + + {label} + {isRequired && *} + + )} + + }> + + + + {fieldConfigItem.description && ( + {fieldConfigItem.description} + )} + + + ); +} + +function ModelPickerView({ + field, + fieldProps, +}: Pick) { + const customOverride = React.useMemo(() => { + const customOnChange = (value: z.infer) => { + field.onChange(value[0]?.url); + }; + return { + ...field, + onChange: customOnChange, + value: field.value + ? [ + { + url: field.value, + }, + ] + : [], + }; + }, [field]); + + return ( +
+ + + { + field.onChange(e.target.value); + }} + type="text" + /> +
+ ); +} diff --git a/web/src/components/custom-form/model-picker.tsx b/web/src/components/custom-form/model-picker.tsx index 7155c53..1620125 100644 --- a/web/src/components/custom-form/model-picker.tsx +++ b/web/src/components/custom-form/model-picker.tsx @@ -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({ ); } + +function ModelPickerView({ + field, +}: Pick) { + return ( + + + + Models (ComfyUI Manager & Civitai) + + +
+ + + {/* {field.value.length} selected */} + {field.value && ( + +