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,9 +330,7 @@ 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