feat: add drop down selection for storage

This commit is contained in:
bennykok 2024-01-28 20:41:53 +08:00
parent 3b7db4480b
commit 757c587901
10 changed files with 591 additions and 439 deletions

View File

@ -15,7 +15,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import type { getAllUserModels as getAllUserModels } from "@/server/getAllUserModel"; import type { getAllUserModels } from "@/server/getAllUserModel";
import type { import type {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
@ -46,8 +46,10 @@ export const columns: ColumnDef<ModelItemList>[] = [
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
<Checkbox <Checkbox
checked={table.getIsAllPageRowsSelected() || checked={
(table.getIsSomePageRowsSelected() && "indeterminate")} table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
/> />
@ -79,12 +81,10 @@ export const columns: ColumnDef<ModelItemList>[] = [
const model = row.original; const model = row.original;
return ( return (
<> <>
{ {/*<a
/*<a
className="hover:underline flex gap-2" className="hover:underline flex gap-2"
href={`/storage/${model.id}`} // TODO href={`/storage/${model.id}`} // TODO
>*/ >*/}
}
<span className="truncate max-w-[200px]"> <span className="truncate max-w-[200px]">
{row.original.model_name} {row.original.model_name}
</span> </span>
@ -110,9 +110,13 @@ export const columns: ColumnDef<ModelItemList>[] = [
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<Badge <Badge
variant={row.original.status === "failed" variant={
row.original.status === "failed"
? "red" ? "red"
: (row.original.status === "started" ? "yellow" : "green")} : row.original.status === "started"
? "yellow"
: "green"
}
> >
{row.original.status} {row.original.status}
</Badge> </Badge>
@ -184,10 +188,10 @@ export const columns: ColumnDef<ModelItemList>[] = [
}, },
cell: ({ row }) => { cell: ({ row }) => {
const model_type_map: Record<modelEnumType, any> = { const model_type_map: Record<modelEnumType, any> = {
"checkpoint": "amber", checkpoint: "amber",
"lora": "green", lora: "green",
"embedding": "violet", embedding: "violet",
"vae": "teal", vae: "teal",
}; };
function getBadgeColor(modelType: modelEnumType) { function getBadgeColor(modelType: modelEnumType) {
@ -257,9 +261,8 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[], [],
); );
const [columnVisibility, setColumnVisibility] = React.useState< const [columnVisibility, setColumnVisibility] =
VisibilityState React.useState<VisibilityState>({});
>({});
const [rowSelection, setRowSelection] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({ const table = useReactTable({
@ -286,10 +289,12 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
<div className="flex flex-row w-full items-center py-4"> <div className="flex flex-row w-full items-center py-4">
<Input <Input
placeholder="Filter workflows..." placeholder="Filter workflows..."
value={(table.getColumn("model_name")?.getFilterValue() as string) ?? value={
""} (table.getColumn("model_name")?.getFilterValue() as string) ?? ""
}
onChange={(event) => onChange={(event) =>
table.getColumn("model_name")?.setFilterValue(event.target.value)} table.getColumn("model_name")?.setFilterValue(event.target.value)
}
className="max-w-sm" className="max-w-sm"
/> />
<div className="ml-auto flex gap-2"> <div className="ml-auto flex gap-2">
@ -304,7 +309,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
formSchema={downloadUrlModelSchema} formSchema={downloadUrlModelSchema}
fieldConfig={{ fieldConfig={{
url: { url: {
fieldType: "fallback", fieldType: "modelUrlPicker",
inputProps: { required: true }, inputProps: { required: true },
description: ( description: (
<> <>
@ -313,6 +318,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
href="https://www.civitai.com/models" href="https://www.civitai.com/models"
target="_blank" target="_blank"
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600" className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
rel="noreferrer"
> >
civitai.com civitai.com
</a>{" "} </a>{" "}
@ -324,9 +330,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
fieldType: "select", fieldType: "select",
inputProps: { required: true }, inputProps: { required: true },
description: ( description: (
<> <>We'll figure this out if you pick a civitai model</>
We'll figure this out if you pick a civitai model
</>
), ),
}, },
}} }}
@ -341,7 +345,9 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender( {header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext(), header.getContext(),
)} )}
@ -352,8 +358,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
))} ))}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows?.length {table.getRowModel().rows?.length ? (
? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow <TableRow
key={row.id} key={row.id}
@ -369,8 +374,7 @@ export function ModelList({ data }: { data: ModelItemList[] }) {
))} ))}
</TableRow> </TableRow>
)) ))
) ) : (
: (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={columns.length} colSpan={columns.length}

View 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}
/>
);
}

View 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,
});

View File

@ -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"
/>
);
}

View File

@ -1,160 +1,17 @@
"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 { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } 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 { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useRef } from "react"; import { CivitaiModelRegistry } from "./CivitaiModelRegistry";
import { useDebouncedCallback } from "use-debounce"; import { ComfyUIManagerModelRegistry } from "./ComfyUIManagerModelRegistry";
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,
});
export function ModelPickerView({ export function ModelPickerView({
field, field,
@ -187,240 +44,3 @@ export function ModelPickerView({
</Accordion> </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>
);
}

View 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>
);
}

View 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;
}

View 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>
);
}

View File

@ -1,3 +1,5 @@
"use client";
import type { AutoFormInputComponentProps } from "../ui/auto-form/types"; import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { import {
FormControl, FormControl,
@ -7,10 +9,19 @@ import {
FormMessage, FormMessage,
} from "../ui/form"; } from "../ui/form";
import { LoadingIcon } from "@/components/LoadingIcon"; import { LoadingIcon } from "@/components/LoadingIcon";
import { ModelPickerView } from "@/components/custom-form/ModelPickerView";
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; // import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import * as React from "react"; import * as React from "react";
import { Suspense } 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({ export default function AutoFormModelsPicker({
label, label,
@ -39,3 +50,35 @@ export default function AutoFormModelsPicker({
</FormItem> </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>
);
}

View File

@ -9,6 +9,7 @@ import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea"; import AutoFormTextarea from "./fields/textarea";
import AutoFormModelsPicker from "@/components/custom-form/model-picker"; import AutoFormModelsPicker from "@/components/custom-form/model-picker";
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker"; import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
import AutoFormModelsPickerUrl from "@/components/custom-form/model-picker-url-only";
export const INPUT_COMPONENTS = { export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox, checkbox: AutoFormCheckbox,
@ -24,6 +25,7 @@ export const INPUT_COMPONENTS = {
snapshot: AutoFormSnapshotPicker, snapshot: AutoFormSnapshotPicker,
models: AutoFormModelsPicker, models: AutoFormModelsPicker,
gpuPicker: AutoFormGPUPicker, gpuPicker: AutoFormGPUPicker,
modelUrlPicker: AutoFormModelsPickerUrl,
}; };
/** /**