diff --git a/ui/admin/app/components/form/controlledInputs.tsx b/ui/admin/app/components/form/controlledInputs.tsx index 8364a299..38677c82 100644 --- a/ui/admin/app/components/form/controlledInputs.tsx +++ b/ui/admin/app/components/form/controlledInputs.tsx @@ -40,7 +40,16 @@ type BaseProps< export type ControlledInputProps< TValues extends FieldValues, TName extends FieldPath, -> = InputProps & BaseProps; +> = InputProps & + BaseProps & { + classNames?: { + wrapper?: string; + label?: string; + input?: string; + description?: string; + message?: string; + }; + }; export function ControlledInput< TValues extends FieldValues, @@ -52,6 +61,7 @@ export function ControlledInput< className, description, onChange, + classNames = {}, ...inputProps }: ControlledInputProps) { return ( @@ -59,8 +69,12 @@ export function ControlledInput< control={control} name={name} render={({ field, fieldState }) => ( - - {label && {label}} + + {label && ( + + {label} + + )} - + {description && ( - {description} + + {description} + )} )} diff --git a/ui/admin/app/components/workflow/ParamsForm.tsx b/ui/admin/app/components/workflow/ParamsForm.tsx index 3e362ea0..4af84668 100644 --- a/ui/admin/app/components/workflow/ParamsForm.tsx +++ b/ui/admin/app/components/workflow/ParamsForm.tsx @@ -1,47 +1,81 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus, TrashIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import { PlusIcon, TrashIcon } from "lucide-react"; +import { useEffect, useMemo } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; import { z } from "zod"; import { Workflow } from "~/lib/model/workflows"; -import { noop } from "~/lib/utils"; +import { ControlledInput } from "~/components/form/controlledInputs"; import { Button } from "~/components/ui/button"; -import { Form, FormField, FormItem, FormMessage } from "~/components/ui/form"; -import { Input } from "~/components/ui/input"; +import { Form } from "~/components/ui/form"; const formSchema = z.object({ - params: z.record(z.string(), z.string()).optional(), + params: z.array( + z.object({ + name: z.string(), + description: z.string(), + }) + ), }); export type ParamFormValues = z.infer; +type ParamValues = Workflow["params"]; + +const convertFrom = (params: ParamValues) => { + const converted = Object.entries(params || {}).map( + ([name, description]) => ({ + name, + description, + }) + ); + + return { + params: converted.length ? converted : [{ name: "", description: "" }], + }; +}; + +const convertTo = (params: ParamFormValues["params"]) => { + if (!params?.length) return undefined; + + return params.reduce((acc, param) => { + if (!param.name) return acc; + + acc[param.name] = param.description; + return acc; + }, {} as NonNullable); +}; + export function ParamsForm({ workflow, - onSubmit, onChange, }: { workflow: Workflow; - onSubmit?: (values: ParamFormValues) => void; - onChange?: (values: ParamFormValues) => void; + onChange?: (values: { params?: ParamValues }) => void; }) { + const defaultValues = useMemo( + () => convertFrom(workflow.params), + [workflow.params] + ); + const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: { params: workflow.params || {} }, + defaultValues, }); - const handleSubmit = form.handleSubmit(onSubmit || noop); - - const [newParamKey, setNewParamKey] = useState(""); - const [newParamValue, setNewParamValue] = useState(""); + const paramFields = useFieldArray({ + control: form.control, + name: "params", + }); useEffect(() => { const subscription = form.watch((value, { name, type }) => { if (name === "params" || type === "change") { const { data, success } = formSchema.safeParse(value); + if (success) { - onChange?.(data); + onChange?.({ params: convertTo(data.params) }); } } }); @@ -50,103 +84,47 @@ export function ParamsForm({ return (
- - ( - -
- - setNewParamKey(e.target.value) - } - className="flex-grow" - /> - - setNewParamValue(e.target.value) - } - className="flex-grow" - /> - -
- -
- {Object.entries(field.value || {}).map( - ([key, value], index) => ( -
- - { - const updatedParams = { - ...field.value, - }; - updatedParams[key] = - e.target.value; - field.onChange( - updatedParams - ); - }} - className="flex-grow" - /> - -
- ) - )} -
- -
- )} - /> - +
+ {paramFields.fields.map((field, i) => ( +
+ + + + + +
+ ))} + + +
); } diff --git a/ui/admin/app/lib/service/routeService.ts b/ui/admin/app/lib/service/routeService.ts index 4db430d2..d0c78b2e 100644 --- a/ui/admin/app/lib/service/routeService.ts +++ b/ui/admin/app/lib/service/routeService.ts @@ -13,6 +13,9 @@ const QuerySchemas = { workflowId: z.string().nullish(), from: z.enum(["workflows", "agents", "users"]).nullish().catch(null), }), + workflowSchema: z.object({ + threadId: z.string().nullish(), + }), } as const; function parseQuery(search: string, schema: T) { @@ -106,7 +109,7 @@ export const RouteHelperMap = { "/workflows/:workflow": { regex: exactRegex($path("/workflows/:workflow", { workflow: "(.+)" })), path: "/workflows/:workflow", - schema: z.null(), + schema: QuerySchemas.workflowSchema, }, } satisfies Record; diff --git a/ui/admin/app/routes/_auth.workflows.$workflow.tsx b/ui/admin/app/routes/_auth.workflows.$workflow.tsx index eb4d4258..fe2d1e94 100644 --- a/ui/admin/app/routes/_auth.workflows.$workflow.tsx +++ b/ui/admin/app/routes/_auth.workflows.$workflow.tsx @@ -5,10 +5,9 @@ import { useNavigate, } from "@remix-run/react"; import { $path } from "remix-routes"; -import { z } from "zod"; import { WorkflowService } from "~/lib/service/api/workflowService"; -import { RouteService } from "~/lib/service/routeService"; +import { RouteQueryParams, RouteService } from "~/lib/service/routeService"; import { noop } from "~/lib/utils"; import { Chat } from "~/components/chat"; @@ -20,31 +19,29 @@ import { } from "~/components/ui/resizable"; import { Workflow } from "~/components/workflow"; -export type SearchParams = z.infer< - (typeof RouteService.schemas)["/workflows/:workflow"] ->; +export type SearchParams = RouteQueryParams<"workflowSchema">; export const clientLoader = async ({ params, request, }: ClientLoaderFunctionArgs) => { - const { workflow: id } = RouteService.getPathParams( + const { pathParams, query } = RouteService.getRouteInfo( "/workflows/:workflow", + new URL(request.url), params ); - if (!id) { - throw redirect("/threads"); + if (!pathParams.workflow) { + throw redirect($path("/workflows")); } - const workflow = await WorkflowService.getWorkflowById(id).catch(noop); - if (!workflow) throw redirect("/agents"); + const workflow = await WorkflowService.getWorkflowById( + pathParams.workflow + ).catch(noop); - const { threadId } = - RouteService.getQueryParams( - "/workflows/:workflow", - new URL(request.url).search - ) || {}; + if (!workflow) throw redirect($path("/workflows")); + + const { threadId } = query ?? {}; return { workflow, threadId }; };