Skip to content

Commit

Permalink
chore: rework params section in workflow form
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhopperlowe committed Nov 21, 2024
1 parent 294c519 commit e75d924
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 135 deletions.
29 changes: 23 additions & 6 deletions ui/admin/app/components/form/controlledInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ type BaseProps<
export type ControlledInputProps<
TValues extends FieldValues,
TName extends FieldPath<TValues>,
> = InputProps & BaseProps<TValues, TName>;
> = InputProps &
BaseProps<TValues, TName> & {
classNames?: {
wrapper?: string;
label?: string;
input?: string;
description?: string;
message?: string;
};
};

export function ControlledInput<
TValues extends FieldValues,
Expand All @@ -52,15 +61,20 @@ export function ControlledInput<
className,
description,
onChange,
classNames = {},
...inputProps
}: ControlledInputProps<TValues, TName>) {
return (
<FormField
control={control}
name={name}
render={({ field, fieldState }) => (
<FormItem>
{label && <FormLabel>{label}</FormLabel>}
<FormItem className={classNames.wrapper}>
{label && (
<FormLabel className={classNames.label}>
{label}
</FormLabel>
)}

<FormControl>
<Input
Expand All @@ -72,15 +86,18 @@ export function ControlledInput<
}}
className={cn(
getFieldStateClasses(fieldState),
className
className,
classNames.input
)}
/>
</FormControl>

<FormMessage />
<FormMessage className={classNames.message} />

{description && (
<FormDescription>{description}</FormDescription>
<FormDescription className={classNames.description}>
{description}
</FormDescription>
)}
</FormItem>
)}
Expand Down
204 changes: 91 additions & 113 deletions ui/admin/app/components/workflow/ParamsForm.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof formSchema>;

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

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<ParamFormValues>({
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) });
}
}
});
Expand All @@ -50,103 +84,47 @@ export function ParamsForm({

return (
<Form {...form}>
<form onSubmit={handleSubmit}>
<FormField
control={form.control}
name="params"
render={({ field }) => (
<FormItem>
<div className="flex space-x-2">
<Input
placeholder="Name"
value={newParamKey}
onChange={(e) =>
setNewParamKey(e.target.value)
}
className="flex-grow"
/>
<Input
placeholder="Description"
value={newParamValue}
onChange={(e) =>
setNewParamValue(e.target.value)
}
className="flex-grow"
/>
<Button
type="button"
size="icon"
className="flex-shrink-0"
variant="secondary"
onClick={() => {
if (newParamKey && newParamValue) {
const updatedParams = {
...field.value,
};
updatedParams[newParamKey] =
newParamValue;
field.onChange(updatedParams);
setNewParamKey("");
setNewParamValue("");
}
}}
>
<Plus className="w-4 h-4" />
</Button>
</div>

<div className="mt-2 w-full">
{Object.entries(field.value || {}).map(
([key, value], index) => (
<div
key={index}
className="flex items-center space-x-2 justify-between mt-2"
>
<Input
disabled
className="cursor-not-allowed"
value={key}
/>
<Input
value={value}
onChange={(e) => {
const updatedParams = {
...field.value,
};
updatedParams[key] =
e.target.value;
field.onChange(
updatedParams
);
}}
className="flex-grow"
/>
<Button
type="button"
variant="destructive"
size="icon"
className="flex-shrink-0"
onClick={() => {
const updatedParams = {
...field.value,
};
delete updatedParams[key];
field.onChange(
updatedParams
);
}}
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
)
)}
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
<div className="flex flex-col gap-4">
{paramFields.fields.map((field, i) => (
<div
className="flex gap-2 p-2 bg-secondary rounded-md"
key={field.id}
>
<ControlledInput
control={form.control}
name={`params.${i}.name`}
placeholder="Name"
classNames={{ wrapper: "flex-auto bg-background" }}
/>

<ControlledInput
control={form.control}
name={`params.${i}.description`}
placeholder="Description"
classNames={{ wrapper: "flex-auto bg-background" }}
/>

<Button
variant="ghost"
size="icon"
onClick={() => paramFields.remove(i)}
>
<TrashIcon />
</Button>
</div>
))}

<Button
variant="secondary"
className="self-end"
startContent={<PlusIcon />}
onClick={() =>
paramFields.append({ name: "", description: "" })
}
>
Add Parameter
</Button>
</div>
</Form>
);
}
5 changes: 4 additions & 1 deletion ui/admin/app/lib/service/routeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends ZodType>(search: string, schema: T) {
Expand Down Expand Up @@ -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<keyof Routes, RouteHelper>;

Expand Down
27 changes: 12 additions & 15 deletions ui/admin/app/routes/_auth.workflows.$workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 };
};
Expand Down

0 comments on commit e75d924

Please sign in to comment.