Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ui/feat/workflow-chat #557

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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