diff --git a/web/bun.lockb b/web/bun.lockb
index 88b5cc2..a803782 100755
Binary files a/web/bun.lockb and b/web/bun.lockb differ
diff --git a/web/package.json b/web/package.json
index 3757de7..4785091 100644
--- a/web/package.json
+++ b/web/package.json
@@ -25,15 +25,21 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
+ "@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-table": "^8.10.7",
"@types/jsonwebtoken": "^9.0.5",
"@types/uuid": "^9.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
+ "date-fns": "^3.0.5",
"dayjs": "^1.11.10",
"drizzle-orm": "^0.29.1",
"jsonwebtoken": "^9.0.2",
@@ -43,6 +49,7 @@
"next-plausible": "^3.12.0",
"next-usequerystate": "^1.13.2",
"react": "^18",
+ "react-day-picker": "^8.9.1",
"react-dom": "^18",
"react-hook-form": "^7.48.2",
"react-use-websocket": "^4.5.0",
diff --git a/web/src/components/RunInputs.tsx b/web/src/components/RunInputs.tsx
index 7e24f42..79b80f9 100644
--- a/web/src/components/RunInputs.tsx
+++ b/web/src/components/RunInputs.tsx
@@ -1,5 +1,3 @@
-import { OutputRender } from "./OutputRender";
-import { CodeBlock } from "@/components/CodeBlock";
import {
Table,
TableBody,
@@ -8,8 +6,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
-import { findAllRuns } from "@/server/findAllRuns";
-import { getRunsOutput } from "@/server/getRunsOutput";
+import type { findAllRuns } from "@/server/findAllRuns";
export async function RunInputs({
run,
@@ -30,16 +27,28 @@ export async function RunInputs({
{Object.entries(run.workflow_inputs).map(([key, data]) => {
let imageUrl;
try {
- const url = new URL(data);
- if (url.pathname.endsWith('.png')) {
+ if (data.startsWith("data:image/")) {
imageUrl = data;
+ } else {
+ const url = new URL(data);
+ if (url.pathname.endsWith(".png")) {
+ imageUrl = data;
+ }
}
- } catch (_) {
- }
+ } catch (_) {}
return (
{key}
- {imageUrl ?
: {data}}
+ {imageUrl ? (
+
+
+
+ ) : (
+ {data}
+ )}
);
})}
diff --git a/web/src/components/VersionSelect.tsx b/web/src/components/VersionSelect.tsx
index ccf117e..b51e696 100644
--- a/web/src/components/VersionSelect.tsx
+++ b/web/src/components/VersionSelect.tsx
@@ -2,7 +2,16 @@
import { callServerPromise } from "./callServerPromise";
import { LoadingIcon } from "@/components/LoadingIcon";
+import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -18,14 +27,16 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import { createRun } from "@/server/createRun";
import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { Copy, MoreVertical, Play } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { toast } from "sonner";
+import { z } from "zod";
export function VersionSelect({
workflow,
@@ -106,31 +117,95 @@ export function RunWorkflowButton({
defaultValue: machines[0].id ?? "",
});
const [isLoading, setIsLoading] = useState(false);
- return (
-
+ const [values, setValues] = useState>({});
+ const [open, setOpen] = useState(false);
+
+ const schema = useMemo(() => {
+ const workflow_version = getWorkflowVersionFromVersionIndex(
+ workflow,
+ version
+ );
+ const inputs = getInputsFromWorkflow(workflow_version);
+
+ if (!inputs) return null;
+
+ return z.object({
+ ...Object.fromEntries(
+ inputs?.map((x) => {
+ return [x?.input_id, z.string().optional()];
+ })
+ ),
+ });
+ }, [version]);
+
+ const runWorkflow = async () => {
+ console.log(values);
+
+ const val = Object.keys(values).length > 0 ? values : undefined;
+
+ const workflow_version_id = workflow?.versions.find(
+ (x) => x.version === version
+ )?.id;
+ console.log(workflow_version_id);
+ if (!workflow_version_id) return;
+
+ setIsLoading(true);
+ try {
+ const origin = window.location.origin;
+ await callServerPromise(
+ createRun(origin, workflow_version_id, machine, val, true)
+ );
+ // console.log(res.json());
+ setIsLoading(false);
+ } catch (error) {
+ setIsLoading(false);
+ }
+
+ setOpen(false);
+ };
+
+ return (
+
);
}
diff --git a/web/src/components/customInputNodes.tsx b/web/src/components/customInputNodes.tsx
index 95af87f..4adfc5e 100644
--- a/web/src/components/customInputNodes.tsx
+++ b/web/src/components/customInputNodes.tsx
@@ -1,4 +1,5 @@
export const customInputNodes: Record = {
ComfyUIDeployExternalText: "string",
ComfyUIDeployExternalImage: "string - (public image url)",
+ ComfyUIDeployExternalImageAlpha: "string - (public image url)",
};
diff --git a/web/src/components/ui/accordion.tsx b/web/src/components/ui/accordion.tsx
index 24c788c..740b4bc 100644
--- a/web/src/components/ui/accordion.tsx
+++ b/web/src/components/ui/accordion.tsx
@@ -1,12 +1,11 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as AccordionPrimitive from "@radix-ui/react-accordion"
-import { ChevronDown } from "lucide-react"
+import { cn } from "@/lib/utils";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDown } from "lucide-react";
+import * as React from "react";
-import { cn } from "@/lib/utils"
-
-const Accordion = AccordionPrimitive.Root
+const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef,
@@ -17,8 +16,8 @@ const AccordionItem = React.forwardRef<
className={cn("border-b", className)}
{...props}
/>
-))
-AccordionItem.displayName = "AccordionItem"
+));
+AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef,
@@ -37,8 +36,8 @@ const AccordionTrigger = React.forwardRef<
-))
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef,
@@ -51,8 +50,8 @@ const AccordionContent = React.forwardRef<
>
{children}
-))
+));
-AccordionContent.displayName = AccordionPrimitive.Content.displayName
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/web/src/components/ui/auto-form/config.ts b/web/src/components/ui/auto-form/config.ts
new file mode 100644
index 0000000..b94b9b0
--- /dev/null
+++ b/web/src/components/ui/auto-form/config.ts
@@ -0,0 +1,33 @@
+import AutoFormCheckbox from "./fields/checkbox";
+import AutoFormDate from "./fields/date";
+import AutoFormEnum from "./fields/enum";
+import AutoFormInput from "./fields/input";
+import AutoFormNumber from "./fields/number";
+import AutoFormRadioGroup from "./fields/radio-group";
+import AutoFormSwitch from "./fields/switch";
+import AutoFormTextarea from "./fields/textarea";
+
+export const INPUT_COMPONENTS = {
+ checkbox: AutoFormCheckbox,
+ date: AutoFormDate,
+ select: AutoFormEnum,
+ radio: AutoFormRadioGroup,
+ switch: AutoFormSwitch,
+ textarea: AutoFormTextarea,
+ number: AutoFormNumber,
+ fallback: AutoFormInput,
+};
+
+/**
+ * Define handlers for specific Zod types.
+ * You can expand this object to support more types.
+ */
+export const DEFAULT_ZOD_HANDLERS: {
+ [key: string]: keyof typeof INPUT_COMPONENTS;
+} = {
+ ZodBoolean: "checkbox",
+ ZodDate: "date",
+ ZodEnum: "select",
+ ZodNativeEnum: "select",
+ ZodNumber: "number",
+};
diff --git a/web/src/components/ui/auto-form/fields/array.tsx b/web/src/components/ui/auto-form/fields/array.tsx
new file mode 100644
index 0000000..1e787e5
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/array.tsx
@@ -0,0 +1,67 @@
+import type { useForm , useForm } from "react-hook-form";
+import { useFieldArray } from "react-hook-form";
+import { Button } from "../../button";
+import { Separator } from "../../separator";
+import { beautifyObjectName } from "../utils";
+import AutoFormObject from "./object";
+import { Plus, Trash } from "lucide-react";
+import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
+import type { z } from "zod";
+
+export default function AutoFormArray({
+ name,
+ item,
+ form,
+ path = [],
+ fieldConfig,
+}: {
+ name: string;
+ item: z.ZodArray;
+ form: ReturnType;
+ path?: string[];
+ fieldConfig?: any;
+}) {
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name,
+ });
+ const title = item._def.description ?? beautifyObjectName(name);
+
+ return (
+
+ {title}
+
+ {fields.map((_field, index) => {
+ const key = [...path, index.toString()].join(".");
+ return (
+
+
}
+ form={form}
+ fieldConfig={fieldConfig}
+ path={[...path, index.toString()]}
+ />
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/checkbox.tsx b/web/src/components/ui/auto-form/fields/checkbox.tsx
new file mode 100644
index 0000000..7087a1e
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/checkbox.tsx
@@ -0,0 +1,32 @@
+import { Checkbox } from "../../checkbox";
+import { FormControl, FormDescription, FormItem, FormLabel } from "../../form";
+import { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormCheckbox({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+
+
+
+
+
+ {label}
+ {isRequired && *}
+
+ {fieldConfigItem.description && (
+ {fieldConfigItem.description}
+ )}
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/date.tsx b/web/src/components/ui/auto-form/fields/date.tsx
new file mode 100644
index 0000000..93197f4
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/date.tsx
@@ -0,0 +1,37 @@
+import { DatePicker } from "../../date-picker";
+import {
+ FormControl,
+ FormDescription,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../form";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormDate({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+
+ {label}
+ {isRequired && *}
+
+
+
+
+ {fieldConfigItem.description && (
+ {fieldConfigItem.description}
+ )}
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/enum.tsx b/web/src/components/ui/auto-form/fields/enum.tsx
new file mode 100644
index 0000000..c529f6e
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/enum.tsx
@@ -0,0 +1,71 @@
+import {
+ FormControl,
+ FormDescription,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../form";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "../../select";
+import { AutoFormInputComponentProps } from "../types";
+import { getBaseSchema } from "../utils";
+import * as z from "zod";
+
+export default function AutoFormEnum({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ zodItem,
+}: AutoFormInputComponentProps) {
+ const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum)._def
+ .values;
+
+ let values: [string, string][] = [];
+ if (!Array.isArray(baseValues)) {
+ values = Object.entries(baseValues);
+ } else {
+ values = baseValues.map((value) => [value, value]);
+ }
+
+ function findItem(value: any) {
+ return values.find((item) => item[0] === value);
+ }
+
+ return (
+
+
+ {label}
+ {isRequired && *}
+
+
+
+
+ {fieldConfigItem.description && (
+ {fieldConfigItem.description}
+ )}
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/input.tsx b/web/src/components/ui/auto-form/fields/input.tsx
new file mode 100644
index 0000000..242556b
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/input.tsx
@@ -0,0 +1,36 @@
+import {
+ FormControl,
+ FormDescription,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../form";
+import { Input } from "../../input";
+import { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormInput({
+ label,
+ isRequired,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
+ const showLabel = _showLabel === undefined ? true : _showLabel;
+ return (
+
+ {showLabel && (
+
+ {label}
+ {isRequired && *}
+
+ )}
+
+
+
+ {fieldConfigItem.description && (
+ {fieldConfigItem.description}
+ )}
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/number.tsx b/web/src/components/ui/auto-form/fields/number.tsx
new file mode 100644
index 0000000..b1f8236
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/number.tsx
@@ -0,0 +1,17 @@
+import { AutoFormInputComponentProps } from "../types";
+import AutoFormInput from "./input";
+
+export default function AutoFormNumber({
+ fieldProps,
+ ...props
+}: AutoFormInputComponentProps) {
+ return (
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/object.tsx b/web/src/components/ui/auto-form/fields/object.tsx
new file mode 100644
index 0000000..9e00789
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/object.tsx
@@ -0,0 +1,130 @@
+import * as z from "zod";
+import { useForm } from "react-hook-form";
+import { FieldConfig, FieldConfigItem } from "../types";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "../../accordion";
+import {
+ beautifyObjectName,
+ getBaseSchema,
+ getBaseType,
+ zodToHtmlInputProps,
+} from "../utils";
+import { FormField } from "../../form";
+import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
+import AutoFormArray from "./array";
+
+function DefaultParent({ children }: { children: React.ReactNode }) {
+ return <>{children}>;
+}
+
+export default function AutoFormObject<
+ SchemaType extends z.ZodObject,
+>({
+ schema,
+ form,
+ fieldConfig,
+ path = [],
+}: {
+ schema: SchemaType | z.ZodEffects;
+ form: ReturnType;
+ fieldConfig?: FieldConfig>;
+ path?: string[];
+}) {
+ const { shape } = getBaseSchema(schema);
+
+ return (
+
+ {Object.keys(shape).map((name) => {
+ const item = shape[name] as z.ZodAny;
+ const zodBaseType = getBaseType(item);
+ const itemName = item._def.description ?? beautifyObjectName(name);
+ const key = [...path, name].join(".");
+
+ if (zodBaseType === "ZodObject") {
+ return (
+
+ {itemName}
+
+ }
+ form={form}
+ fieldConfig={
+ (fieldConfig?.[name] ?? {}) as FieldConfig<
+ z.infer
+ >
+ }
+ path={[...path, name]}
+ />
+
+
+ );
+ }
+ if (zodBaseType === "ZodArray") {
+ return (
+ }
+ form={form}
+ fieldConfig={fieldConfig?.[name] ?? {}}
+ path={[...path, name]}
+ />
+ );
+ }
+
+ const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
+ const zodInputProps = zodToHtmlInputProps(item);
+ const isRequired =
+ zodInputProps.required ||
+ fieldConfigItem.inputProps?.required ||
+ false;
+
+ return (
+ {
+ const inputType =
+ fieldConfigItem.fieldType ??
+ DEFAULT_ZOD_HANDLERS[zodBaseType] ??
+ "fallback";
+
+ const InputComponent =
+ typeof inputType === "function"
+ ? inputType
+ : INPUT_COMPONENTS[inputType];
+ const ParentElement =
+ fieldConfigItem.renderParent ?? DefaultParent;
+
+ return (
+
+
+
+ );
+ }}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/radio-group.tsx b/web/src/components/ui/auto-form/fields/radio-group.tsx
new file mode 100644
index 0000000..7ea7840
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/radio-group.tsx
@@ -0,0 +1,44 @@
+import * as z from "zod";
+import { AutoFormInputComponentProps } from "../types";
+import { FormControl, FormItem, FormLabel, FormMessage } from "../../form";
+import { RadioGroup, RadioGroupItem } from "../../radio-group";
+
+export default function AutoFormRadioGroup({
+ label,
+ isRequired,
+ field,
+ zodItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const values = (zodItem as unknown as z.ZodEnum)._def.values;
+
+ return (
+
+
+ {label}
+ {isRequired && *}
+
+
+
+ {values.map((value: any) => (
+
+
+
+
+ {value}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/switch.tsx b/web/src/components/ui/auto-form/fields/switch.tsx
new file mode 100644
index 0000000..43df3f6
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/switch.tsx
@@ -0,0 +1,32 @@
+import { FormControl, FormDescription, FormItem, FormLabel } from "../../form";
+import { Switch } from "../../switch";
+import { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormSwitch({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+
+
+
+
+
+ {label}
+ {isRequired && *}
+
+ {fieldConfigItem.description && (
+ {fieldConfigItem.description}
+ )}
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/fields/textarea.tsx b/web/src/components/ui/auto-form/fields/textarea.tsx
new file mode 100644
index 0000000..92dece7
--- /dev/null
+++ b/web/src/components/ui/auto-form/fields/textarea.tsx
@@ -0,0 +1,36 @@
+import {
+ FormControl,
+ FormDescription,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "../../form";
+import { Textarea } from "../../textarea";
+import { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormTextarea({
+ label,
+ isRequired,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
+ const showLabel = _showLabel === undefined ? true : _showLabel;
+ return (
+
+ {showLabel && (
+
+ {label}
+ {isRequired && *}
+
+ )}
+
+
+
+ {fieldConfigItem.description && (
+ {fieldConfigItem.description}
+ )}
+
+
+ );
+}
diff --git a/web/src/components/ui/auto-form/index.tsx b/web/src/components/ui/auto-form/index.tsx
new file mode 100644
index 0000000..668cd16
--- /dev/null
+++ b/web/src/components/ui/auto-form/index.tsx
@@ -0,0 +1,87 @@
+"use client";
+import React from "react";
+import { z } from "zod";
+import { Form } from "../form";
+import { DefaultValues, useForm } from "react-hook-form";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Button } from "../button";
+import { cn } from "@/lib/utils";
+
+import { FieldConfig } from "./types";
+import {
+ ZodObjectOrWrapped,
+ getDefaultValues,
+ getObjectFormSchema,
+} from "./utils";
+import AutoFormObject from "./fields/object";
+
+export function AutoFormSubmit({ children }: { children?: React.ReactNode }) {
+ return ;
+}
+
+function AutoForm({
+ formSchema,
+ values: valuesProp,
+ onValuesChange: onValuesChangeProp,
+ onParsedValuesChange,
+ onSubmit: onSubmitProp,
+ fieldConfig,
+ children,
+ className,
+}: {
+ formSchema: SchemaType;
+ values?: Partial>;
+ onValuesChange?: (values: Partial>) => void;
+ onParsedValuesChange?: (values: Partial>) => void;
+ onSubmit?: (values: z.infer) => void;
+ fieldConfig?: FieldConfig>;
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ const objectFormSchema = getObjectFormSchema(formSchema);
+ const defaultValues: DefaultValues> =
+ getDefaultValues(objectFormSchema);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues,
+ values: valuesProp,
+ });
+
+ function onSubmit(values: z.infer) {
+ const parsedValues = formSchema.safeParse(values);
+ if (parsedValues.success) {
+ onSubmitProp?.(parsedValues.data);
+ }
+ }
+
+ return (
+
+
+ );
+}
+
+export default AutoForm;
diff --git a/web/src/components/ui/auto-form/types.ts b/web/src/components/ui/auto-form/types.ts
new file mode 100644
index 0000000..d0e2ab6
--- /dev/null
+++ b/web/src/components/ui/auto-form/types.ts
@@ -0,0 +1,37 @@
+import { ControllerRenderProps, FieldValues } from "react-hook-form";
+import * as z from "zod";
+import { INPUT_COMPONENTS } from "./config";
+
+export type FieldConfigItem = {
+ description?: React.ReactNode;
+ inputProps?: React.InputHTMLAttributes & {
+ showLabel?: boolean;
+ };
+ fieldType?:
+ | keyof typeof INPUT_COMPONENTS
+ | React.FC;
+
+ renderParent?: (props: {
+ children: React.ReactNode;
+ }) => React.ReactElement | null;
+};
+
+export type FieldConfig>> = {
+ // If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
+ [Key in keyof SchemaType]?: SchemaType[Key] extends object
+ ? FieldConfig>
+ : FieldConfigItem;
+};
+
+/**
+ * A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
+ */
+export type AutoFormInputComponentProps = {
+ zodInputProps: React.InputHTMLAttributes;
+ field: ControllerRenderProps;
+ fieldConfigItem: FieldConfigItem;
+ label: string;
+ isRequired: boolean;
+ fieldProps: any;
+ zodItem: z.ZodAny;
+};
diff --git a/web/src/components/ui/auto-form/utils.ts b/web/src/components/ui/auto-form/utils.ts
new file mode 100644
index 0000000..517447a
--- /dev/null
+++ b/web/src/components/ui/auto-form/utils.ts
@@ -0,0 +1,159 @@
+import { DefaultValues } from "react-hook-form";
+import { z } from "zod";
+
+// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
+export type ZodObjectOrWrapped =
+ | z.ZodObject
+ | z.ZodEffects>;
+
+/**
+ * Beautify a camelCase string.
+ * e.g. "myString" -> "My String"
+ */
+export function beautifyObjectName(string: string) {
+ let output = string.replace(/([A-Z])/g, " $1");
+ output = output.charAt(0).toUpperCase() + output.slice(1);
+ return output;
+}
+
+/**
+ * Get the lowest level Zod type.
+ * This will unpack optionals, refinements, etc.
+ */
+export function getBaseSchema<
+ ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
+>(schema: ChildType | z.ZodEffects): ChildType {
+ if ("innerType" in schema._def) {
+ return getBaseSchema(schema._def.innerType as ChildType);
+ }
+ if ("schema" in schema._def) {
+ return getBaseSchema(schema._def.schema as ChildType);
+ }
+ return schema as ChildType;
+}
+
+/**
+ * Get the type name of the lowest level Zod type.
+ * This will unpack optionals, refinements, etc.
+ */
+export function getBaseType(schema: z.ZodAny): string {
+ return getBaseSchema(schema)._def.typeName;
+}
+
+/**
+ * Search for a "ZodDefult" in the Zod stack and return its value.
+ */
+export function getDefaultValueInZodStack(schema: z.ZodAny): any {
+ const typedSchema = schema as unknown as z.ZodDefault<
+ z.ZodNumber | z.ZodString
+ >;
+
+ if (typedSchema._def.typeName === "ZodDefault") {
+ return typedSchema._def.defaultValue();
+ }
+
+ if ("innerType" in typedSchema._def) {
+ return getDefaultValueInZodStack(
+ typedSchema._def.innerType as unknown as z.ZodAny,
+ );
+ }
+ if ("schema" in typedSchema._def) {
+ return getDefaultValueInZodStack(
+ (typedSchema._def as any).schema as z.ZodAny,
+ );
+ }
+ return undefined;
+}
+
+/**
+ * Get all default values from a Zod schema.
+ */
+export function getDefaultValues>(
+ schema: Schema,
+) {
+ const { shape } = schema;
+ type DefaultValuesType = DefaultValues>>;
+ const defaultValues = {} as DefaultValuesType;
+
+ for (const key of Object.keys(shape)) {
+ const item = shape[key] as z.ZodAny;
+
+ if (getBaseType(item) === "ZodObject") {
+ const defaultItems = getDefaultValues(
+ getBaseSchema(item) as unknown as z.ZodObject,
+ );
+ for (const defaultItemKey of Object.keys(defaultItems)) {
+ const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
+ defaultValues[pathKey] = defaultItems[defaultItemKey];
+ }
+ } else {
+ const defaultValue = getDefaultValueInZodStack(item);
+ if (defaultValue !== undefined) {
+ defaultValues[key as keyof DefaultValuesType] = defaultValue;
+ }
+ }
+ }
+
+ return defaultValues;
+}
+
+export function getObjectFormSchema(
+ schema: ZodObjectOrWrapped,
+): z.ZodObject {
+ if (schema._def.typeName === "ZodEffects") {
+ const typedSchema = schema as z.ZodEffects>;
+ return getObjectFormSchema(typedSchema._def.schema);
+ }
+ return schema as z.ZodObject;
+}
+
+/**
+ * Convert a Zod schema to HTML input props to give direct feedback to the user.
+ * Once submitted, the schema will be validated completely.
+ */
+export function zodToHtmlInputProps(
+ schema:
+ | z.ZodNumber
+ | z.ZodString
+ | z.ZodOptional
+ | any,
+): React.InputHTMLAttributes {
+ if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
+ const typedSchema = schema as z.ZodOptional;
+ return {
+ ...zodToHtmlInputProps(typedSchema._def.innerType),
+ required: false,
+ };
+ }
+
+ const typedSchema = schema as z.ZodNumber | z.ZodString;
+
+ if (!("checks" in typedSchema._def)) return {
+ required: true
+ };
+
+ const { checks } = typedSchema._def;
+ const inputProps: React.InputHTMLAttributes = {
+ required: true,
+ };
+ const type = getBaseType(schema);
+
+ for (const check of checks) {
+ if (check.kind === "min") {
+ if (type === "ZodString") {
+ inputProps.minLength = check.value;
+ } else {
+ inputProps.min = check.value;
+ }
+ }
+ if (check.kind === "max") {
+ if (type === "ZodString") {
+ inputProps.maxLength = check.value;
+ } else {
+ inputProps.max = check.value;
+ }
+ }
+ }
+
+ return inputProps;
+}
diff --git a/web/src/components/ui/calendar.tsx b/web/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2f02434
--- /dev/null
+++ b/web/src/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/web/src/components/ui/date-picker.tsx b/web/src/components/ui/date-picker.tsx
new file mode 100644
index 0000000..a841541
--- /dev/null
+++ b/web/src/components/ui/date-picker.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Calendar } from "@/components/ui/calendar";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+import { format } from "date-fns";
+import { Calendar as CalendarIcon } from "lucide-react";
+import { forwardRef } from "react";
+
+export const DatePicker = forwardRef<
+ HTMLDivElement,
+ {
+ date?: Date;
+ setDate: (date?: Date) => void;
+ }
+>(function DatePickerCmp({ date, setDate }, ref) {
+ return (
+
+
+
+
+
+
+
+
+ );
+});
diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx
index 4603f8b..603874c 100644
--- a/web/src/components/ui/form.tsx
+++ b/web/src/components/ui/form.tsx
@@ -1,30 +1,23 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { Slot } from "@radix-ui/react-slot"
-import {
- Controller,
- ControllerProps,
- FieldPath,
- FieldValues,
- FormProvider,
- useFormContext,
-} from "react-hook-form"
+import { Label } from "@/components/ui/label";
+import { cn } from "@/lib/utils";
+import type * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import * as React from "react";
+import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
+import { Controller, FormProvider, useFormContext } from "react-hook-form";
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
-
-const Form = FormProvider
+const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath
> = {
- name: TName
-}
+ name: TName;
+};
const FormFieldContext = React.createContext(
{} as FormFieldContextValue
-)
+);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -36,21 +29,21 @@ const FormField = <
- )
-}
+ );
+};
const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext)
- const itemContext = React.useContext(FormItemContext)
- const { getFieldState, formState } = useFormContext()
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
- const fieldState = getFieldState(fieldContext.name, formState)
+ const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
- throw new Error("useFormField should be used within ")
+ throw new Error("useFormField should be used within ");
}
- const { id } = itemContext
+ const { id } = itemContext;
return {
id,
@@ -59,36 +52,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
- }
-}
+ };
+};
type FormItemContextValue = {
- id: string
-}
+ id: string;
+};
const FormItemContext = React.createContext(
{} as FormItemContextValue
-)
+);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const id = React.useId()
+ const id = React.useId();
return (
- )
-})
-FormItem.displayName = "FormItem"
+ );
+});
+FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
- const { error, formItemId } = useFormField()
+ const { error, formItemId } = useFormField();
return (
- )
-})
-FormLabel.displayName = "FormLabel"
+ );
+});
+FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
return (
- )
-})
-FormControl.displayName = "FormControl"
+ );
+});
+FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const { formDescriptionId } = useFormField()
+ const { formDescriptionId } = useFormField();
return (
- )
-})
-FormDescription.displayName = "FormDescription"
+ );
+});
+FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
- const { error, formMessageId } = useFormField()
- const body = error ? String(error?.message) : children
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
if (!body) {
- return null
+ return null;
}
return (
@@ -160,9 +154,9 @@ const FormMessage = React.forwardRef<
>
{body}
- )
-})
-FormMessage.displayName = "FormMessage"
+ );
+});
+FormMessage.displayName = "FormMessage";
export {
useFormField,
@@ -173,4 +167,4 @@ export {
FormDescription,
FormMessage,
FormField,
-}
+};
diff --git a/web/src/components/ui/popover.tsx b/web/src/components/ui/popover.tsx
new file mode 100644
index 0000000..a0ec48b
--- /dev/null
+++ b/web/src/components/ui/popover.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/web/src/components/ui/radio-group.tsx b/web/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000..e9bde17
--- /dev/null
+++ b/web/src/components/ui/radio-group.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const RadioGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ )
+})
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+
+export { RadioGroup, RadioGroupItem }
diff --git a/web/src/components/ui/separator.tsx b/web/src/components/ui/separator.tsx
new file mode 100644
index 0000000..12d81c4
--- /dev/null
+++ b/web/src/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/web/src/components/ui/switch.tsx b/web/src/components/ui/switch.tsx
new file mode 100644
index 0000000..bc69cf2
--- /dev/null
+++ b/web/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/web/src/components/ui/textarea.tsx b/web/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..9f9a6dc
--- /dev/null
+++ b/web/src/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/web/src/components/ui/toggle.tsx b/web/src/components/ui/toggle.tsx
new file mode 100644
index 0000000..b6f2e7d
--- /dev/null
+++ b/web/src/components/ui/toggle.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import * as React from "react"
+import * as TogglePrimitive from "@radix-ui/react-toggle"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const toggleVariants = cva(
+ "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline:
+ "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
+ },
+ size: {
+ default: "h-10 px-3",
+ sm: "h-9 px-2.5",
+ lg: "h-11 px-5",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+const Toggle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, size, ...props }, ref) => (
+
+))
+
+Toggle.displayName = TogglePrimitive.Root.displayName
+
+export { Toggle, toggleVariants }