Skip to content

Commit

Permalink
Ui/feat/workflow-chat (#557)
Browse files Browse the repository at this point in the history
* feat: implement workflow chat interface

* chore: rework params section in workflow form

* feat: add params form to workflow invoke button

* fix: load workflow directly when invoking

* feat: move workflow param form to invoke button on inital trigger
  • Loading branch information
ryanhopperlowe authored Nov 25, 2024
1 parent 1faeb44 commit 6254fe3
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 180 deletions.
46 changes: 34 additions & 12 deletions ui/admin/app/components/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import { useState } from "react";

import { cn } from "~/lib/utils";

import { useChat } from "~/components/chat/ChatContext";
import { Chatbar } from "~/components/chat/Chatbar";
import { MessagePane } from "~/components/chat/MessagePane";
import { Button } from "~/components/ui/button";
import { RunWorkflow } from "~/components/chat/RunWorkflow";

type ChatProps = React.HTMLAttributes<HTMLDivElement> & {
showStartButton?: boolean;
};

export function Chat({ className, showStartButton = false }: ChatProps) {
const { messages, threadId, mode, invoke, readOnly } = useChat();
export function Chat({ className }: ChatProps) {
const {
id,
messages,
threadId,
mode,
invoke,
readOnly,
isInvoking,
isRunning,
} = useChat();
const [runTriggered, setRunTriggered] = useState(false);

const showMessagePane =
mode === "agent" ||
(mode === "workflow" && (threadId || runTriggered || !showStartButton));
(mode === "workflow" && (threadId || runTriggered || !readOnly));

const showStartButtonPane =
mode === "workflow" && showStartButton && !(threadId || runTriggered);
const showStartButtonPane = mode === "workflow" && !readOnly;

return (
<div className={`flex flex-col h-full ${className}`}>
Expand All @@ -34,16 +44,28 @@ export function Chat({ className, showStartButton = false }: ChatProps) {
{mode === "agent" && !readOnly && <Chatbar className="px-20" />}

{showStartButtonPane && (
<div className="flex justify-center items-center h-full px-20">
<Button
variant="secondary"
onClick={() => {
<div
className={cn("px-20 mb-4", {
"flex justify-center items-center h-full": !threadId,
})}
>
<RunWorkflow
workflowId={id}
onSubmit={(params) => {
setRunTriggered(true);
invoke();
invoke(params && JSON.stringify(params));
}}
className={cn({
"w-full": threadId,
})}
popoverContentProps={{
sideOffset: !threadId ? -150 : undefined,
}}
loading={isInvoking || isRunning}
disabled={isInvoking || isRunning}
>
Run
</Button>
</RunWorkflow>
</div>
)}
</div>
Expand Down
35 changes: 7 additions & 28 deletions ui/admin/app/components/chat/ChatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Mode = "agent" | "workflow";
interface ChatContextType {
messages: Message[];
mode: Mode;
processUserMessage: (text: string, sender: "user" | "agent") => void;
processUserMessage: (text: string) => void;
id: string;
threadId: Nullish<string>;
invoke: (prompt?: string) => void;
Expand All @@ -46,38 +46,17 @@ export function ChatProvider({
onCreateThreadId?: (threadId: string) => void;
readOnly?: boolean;
}) {
/**
* processUserMessage is responsible for adding the user's message to the chat and
* triggering the agent to respond to it.
*/
const processUserMessage = (text: string, sender: "user" | "agent") => {
if (mode === "workflow" || readOnly) return;
const newMessage: Message = { text, sender };

// insertMessage(newMessage);
handlePrompt(newMessage.text);
};

const invoke = (prompt?: string) => {
if (prompt && mode === "agent" && !readOnly) {
handlePrompt(prompt);
}
};
if (readOnly) return;

const handlePrompt = (prompt: string) => {
if (prompt && mode === "agent" && !readOnly) {
invokeAgent.execute({
slug: id,
prompt: prompt,
thread: threadId,
});
}
// do nothing if the mode is workflow
if (mode === "workflow") invokeAgent.execute({ slug: id, prompt });
else if (mode === "agent")
invokeAgent.execute({ slug: id, prompt, thread: threadId });
};

const invokeAgent = useAsync(InvokeService.invokeAgentWithStream, {
onSuccess: ({ threadId: responseThreadId }) => {
if (responseThreadId && !threadId) {
if (responseThreadId && responseThreadId !== threadId) {
// persist the threadId
onCreateThreadId?.(responseThreadId);

Expand All @@ -93,7 +72,7 @@ export function ChatProvider({
<ChatContext.Provider
value={{
messages,
processUserMessage,
processUserMessage: invoke,
mode,
id,
threadId,
Expand Down
2 changes: 1 addition & 1 deletion ui/admin/app/components/chat/Chatbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Chatbar({ className }: ChatbarProps) {
if (isRunning) return;

if (input.trim()) {
processUserMessage(input, "user");
processUserMessage(input);
setInput("");
}
};
Expand Down
4 changes: 2 additions & 2 deletions ui/admin/app/components/chat/MessagePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function MessagePane({
className,
classNames = {},
}: MessagePaneProps) {
const { readOnly, isRunning } = useChat();
const { readOnly, isRunning, mode } = useChat();

const isEmpty = messages.length === 0 && !readOnly;
const isEmpty = messages.length === 0 && !readOnly && mode === "agent";

return (
<div className={cn("flex flex-col h-full", className, classNames.root)}>
Expand Down
10 changes: 3 additions & 7 deletions ui/admin/app/components/chat/NoMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { Button } from "~/components/ui/button";
export function NoMessages() {
const { processUserMessage, isInvoking } = useChat();

const handleAddMessage = (content: string) => {
processUserMessage(content, "user");
};

return (
<div className="flex flex-col items-center justify-center space-y-4 text-center p-4 h-full">
<h2 className="text-2xl font-semibold">Start the conversation!</h2>
Expand All @@ -22,7 +18,7 @@ export function NoMessages() {
shape="pill"
disabled={isInvoking}
onClick={() =>
handleAddMessage(
processUserMessage(
"Tell me who you are and what your objectives are."
)
}
Expand All @@ -35,7 +31,7 @@ export function NoMessages() {
shape="pill"
disabled={isInvoking}
onClick={() =>
handleAddMessage(
processUserMessage(
"Tell me what tools you have available."
)
}
Expand All @@ -48,7 +44,7 @@ export function NoMessages() {
shape="pill"
disabled={isInvoking}
onClick={() =>
handleAddMessage(
processUserMessage(
"Using your knowledge tools, tell me about your knowledge set."
)
}
Expand Down
73 changes: 73 additions & 0 deletions ui/admin/app/components/chat/RunWorkflow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ComponentProps, useState } from "react";
import useSWR from "swr";

import { WorkflowService } from "~/lib/service/api/workflowService";

import { RunWorkflowForm } from "~/components/chat/RunWorkflowForm";
import { Button, ButtonProps } from "~/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";

type RunWorkflowProps = {
onSubmit: (params?: Record<string, string>) => void;
workflowId: string;
popoverContentProps?: ComponentProps<typeof PopoverContent>;
};

export function RunWorkflow({
workflowId,
onSubmit,
...props
}: RunWorkflowProps & ButtonProps) {
const [open, setOpen] = useState(false);

const { data: workflow, isLoading } = useSWR(
WorkflowService.getWorkflowById.key(workflowId),
({ workflowId }) => WorkflowService.getWorkflowById(workflowId)
);

const params = workflow?.params;

if (!params || isLoading)
return (
<Button
onClick={() => onSubmit()}
{...props}
disabled={props.disabled || isLoading}
loading={isLoading || props.loading}
>
Run Workflow
</Button>
);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
{...props}
disabled={props.disabled || open || isLoading}
loading={props.loading || isLoading}
onClick={() => setOpen((prev) => !prev)}
>
Run Workflow
</Button>
</PopoverTrigger>

<PopoverContent
{...props.popoverContentProps}
className="min-w-full"
>
<RunWorkflowForm
params={params}
onSubmit={(params) => {
setOpen(false);
onSubmit(params);
}}
/>
</PopoverContent>
</Popover>
);
}
44 changes: 44 additions & 0 deletions ui/admin/app/components/chat/RunWorkflowForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useMemo } from "react";
import { useForm } from "react-hook-form";

import { ControlledInput } from "~/components/form/controlledInputs";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";

type RunWorkflowFormProps = {
params: Record<string, string>;
onSubmit: (params: Record<string, string>) => void;
};

export function RunWorkflowForm({ params, onSubmit }: RunWorkflowFormProps) {
const defaultValues = useMemo(() => {
return Object.keys(params).reduce(
(acc, key) => {
acc[key] = "";
return acc;
},
{} as Record<string, string>
);
}, [params]);

const form = useForm({ defaultValues });
const handleSubmit = form.handleSubmit(onSubmit);

return (
<Form {...form}>
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
{Object.entries(params).map(([name, description]) => (
<ControlledInput
key={name}
control={form.control}
name={name}
label={name}
description={description}
/>
))}

<Button type="submit">Run Workflow</Button>
</form>
</Form>
);
}
8 changes: 7 additions & 1 deletion ui/admin/app/components/form/controlledInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ export type ControlledInputProps<
TName extends FieldPath<TValues>,
> = InputProps &
BaseProps<TValues, TName> & {
classNames?: { wrapper?: string };
onChangeConversion?: (value: string) => string;
classNames?: {
wrapper?: string;
label?: string;
input?: string;
description?: string;
message?: string;
};
};

export function ControlledInput<
Expand Down
Loading

0 comments on commit 6254fe3

Please sign in to comment.