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 ( + + + + + + + Run outputs + + You can view your run's outputs here + + + {/*
*/} + {schema && ( + +
+ + Run + + {isLoading ? : } + + +
+
+ )} + {!schema && ( + + )} + {/*
*/} + {/*
{view}
*/} +
+
); } 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 && *} + + )} + +