feat: avoid unnecessary rerenders

- use `react-hook-form` to re-render as little as possible
- switch to uncontrolled state to avoid rerendering whole thread while typing input
This commit is contained in:
Tat Dat Duong
2025-05-12 21:12:22 -07:00
parent accf6461e9
commit 022ef4aed6
9 changed files with 665 additions and 653 deletions
@@ -21,6 +21,7 @@ import {
} from "@/types/configurable";
import _ from "lodash";
import { useFetchPreselectedTools } from "@/hooks/use-fetch-preselected-tools";
import { Controller, useFormContext } from "react-hook-form";
export function AgentFieldsFormLoading() {
return (
@@ -39,32 +40,26 @@ export function AgentFieldsFormLoading() {
}
interface AgentFieldsFormProps {
name: string;
setName: (name: string) => void;
description: string;
setDescription: (description: string) => void;
configurations: ConfigurableFieldUIMetadata[];
toolConfigurations: ConfigurableFieldMCPMetadata[];
config: Record<string, any>;
setConfig: (config: Record<string, any>) => void;
agentId: string;
ragConfigurations: ConfigurableFieldRAGMetadata[];
agentsConfigurations: ConfigurableFieldAgentsMetadata[];
}
export function AgentFieldsForm({
name,
setName,
description,
setDescription,
configurations,
toolConfigurations,
config,
setConfig,
agentId,
ragConfigurations,
agentsConfigurations,
}: AgentFieldsFormProps) {
const form = useFormContext<{
name: string;
description: string;
config: Record<string, any>;
}>();
const { tools, setTools, getTools, cursor, loading } = useMCPContext();
const { toolSearchTerm, debouncedSetSearchTerm, displayTools } =
useSearchTools(tools, {
@@ -81,7 +76,7 @@ export function AgentFieldsForm({
});
return (
<div className="flex flex-col gap-8 overflow-y-auto py-4">
<div className="flex flex-col gap-8 py-4">
<div className="flex w-full flex-col items-start justify-start gap-2 space-y-2">
<p className="text-lg font-semibold tracking-tight">Agent Details</p>
<div className="flex w-full flex-col items-start justify-start gap-2">
@@ -90,8 +85,7 @@ export function AgentFieldsForm({
</Label>
<Input
id="oap_name"
value={name}
onChange={(e) => setName(e.target.value)}
{...form.register("name")}
placeholder="Emails Agent"
/>
</div>
@@ -101,149 +95,170 @@ export function AgentFieldsForm({
</Label>
<Textarea
id="oap_description"
value={description}
onChange={(e) => setDescription(e.target.value)}
{...form.register("description")}
placeholder="Agent that handles emails"
/>
</div>
</div>
{configurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2 space-y-2">
<p className="text-lg font-semibold tracking-tight">
Agent Configuration
</p>
{configurations.map((c, index) => (
<ConfigField
key={`${c.label}-${index}`}
className="w-full"
id={c.label}
label={c.label}
type={c.type === "boolean" ? "switch" : (c.type ?? "text")}
description={c.description}
placeholder={c.placeholder}
options={c.options}
min={c.min}
max={c.max}
step={c.step}
value={config[c.label]}
setValue={(v) => setConfig({ ...config, [c.label]: v })}
agentId={agentId}
/>
))}
</div>
</>
)}
{toolConfigurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2 space-y-2">
<p className="text-lg font-semibold tracking-tight">Agent Tools</p>
<Search
onSearchChange={debouncedSetSearchTerm}
placeholder="Search tools..."
className="w-full"
/>
<div className="max-h-[500px] w-full flex-1 overflow-y-auto rounded-md border-[1px] border-slate-200 px-4">
{toolConfigurations[0]?.label
? displayTools.map((c) => (
<ConfigFieldTool
key={`tool-${c.name}`}
id={c.name}
label={c.name}
<>
{configurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2 space-y-2">
<p className="text-lg font-semibold tracking-tight">
Agent Configuration
</p>
{configurations.map((c, index) => (
<Controller
key={`${c.label}-${index}`}
control={form.control}
name={`config.${c.label}`}
render={({ field: { value, onChange } }) => (
<ConfigField
className="w-full"
id={c.label}
label={c.label}
type={
c.type === "boolean" ? "switch" : (c.type ?? "text")
}
description={c.description}
placeholder={c.placeholder}
options={c.options}
min={c.min}
max={c.max}
step={c.step}
value={value}
setValue={onChange}
agentId={agentId}
toolId={toolConfigurations[0].label}
className="border-b-[1px] py-4"
value={config[toolConfigurations[0].label]}
setValue={(v) =>
setConfig({
...config,
[toolConfigurations[0].label]: v,
})
}
/>
))
: null}
{displayTools.length === 0 && toolSearchTerm && (
<p className="my-4 w-full text-center text-sm text-slate-500">
No tools found matching "{toolSearchTerm}".
</p>
)}
{tools.length === 0 && !toolSearchTerm && (
<p className="my-4 w-full text-center text-sm text-slate-500">
No tools available for this agent.
</p>
)}
{cursor && !toolSearchTerm && (
<div className="flex justify-center py-4">
<Button
variant="outline"
size="sm"
onClick={async () => {
try {
setLoadingMore(true);
const moreTool = await getTools(cursor);
setTools((prevTools) => [...prevTools, ...moreTool]);
} catch (error) {
console.error("Failed to load more tools:", error);
} finally {
setLoadingMore(false);
}
}}
disabled={loadingMore || loading}
>
{loadingMore ? "Loading..." : "Load More Tools"}
</Button>
</div>
)}
)}
/>
))}
</div>
</div>
</>
)}
{ragConfigurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2">
<p className="text-lg font-semibold tracking-tight">Agent RAG</p>
<ConfigFieldRAG
id={ragConfigurations[0].label}
label={ragConfigurations[0].label}
agentId={agentId}
value={config[ragConfigurations[0].label]}
setValue={(v) =>
setConfig({
...config,
[ragConfigurations[0].label]: v,
})
}
/>
</div>
</>
)}
{agentsConfigurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2">
<p className="text-lg font-semibold tracking-tight">
Supervisor Agents
</p>
<ConfigFieldAgents
id={agentsConfigurations[0].label}
label={agentsConfigurations[0].label}
agentId={agentId}
value={config[agentsConfigurations[0].label]}
setValue={(v) =>
setConfig({
...config,
[agentsConfigurations[0].label]: v,
})
}
/>
</div>
</>
)}
</>
)}
{toolConfigurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-4">
<p className="text-lg font-semibold tracking-tight">
Agent Tools
</p>
<Search
onSearchChange={debouncedSetSearchTerm}
placeholder="Search tools..."
className="w-full"
/>
<div className="relative w-full flex-1 basis-[500px] rounded-md border-[1px] border-slate-200 px-4">
<div className="absolute inset-0 overflow-y-auto px-4">
{toolConfigurations[0]?.label
? displayTools.map((c) => (
<Controller
key={`tool-${c.name}`}
control={form.control}
name={`config.${toolConfigurations[0].label}`}
render={({ field: { value, onChange } }) => (
<ConfigFieldTool
key={`tool-${c.name}`}
id={c.name}
label={c.name}
description={c.description}
agentId={agentId}
toolId={toolConfigurations[0].label}
className="border-b-[1px] py-4"
value={value}
setValue={onChange}
/>
)}
/>
))
: null}
{displayTools.length === 0 && toolSearchTerm && (
<p className="my-4 w-full text-center text-sm text-slate-500">
No tools found matching "{toolSearchTerm}".
</p>
)}
{tools.length === 0 && !toolSearchTerm && (
<p className="my-4 w-full text-center text-sm text-slate-500">
No tools available for this agent.
</p>
)}
{cursor && !toolSearchTerm && (
<div className="flex justify-center py-4">
<Button
variant="outline"
size="sm"
onClick={async () => {
try {
setLoadingMore(true);
const moreTool = await getTools(cursor);
setTools((prevTools) => [
...prevTools,
...moreTool,
]);
} catch (error) {
console.error("Failed to load more tools:", error);
} finally {
setLoadingMore(false);
}
}}
disabled={loadingMore || loading}
>
{loadingMore ? "Loading..." : "Load More Tools"}
</Button>
</div>
)}
</div>
</div>
</div>
</>
)}
{ragConfigurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2">
<p className="text-lg font-semibold tracking-tight">Agent RAG</p>
<Controller
control={form.control}
name={`config.${ragConfigurations[0].label}`}
render={({ field: { value, onChange } }) => (
<ConfigFieldRAG
id={ragConfigurations[0].label}
label={ragConfigurations[0].label}
agentId={agentId}
value={value}
setValue={onChange}
/>
)}
/>
</div>
</>
)}
{agentsConfigurations.length > 0 && (
<>
<Separator />
<div className="flex w-full flex-col items-start justify-start gap-2">
<p className="text-lg font-semibold tracking-tight">
Supervisor Agents
</p>
<Controller
control={form.control}
name={`config.${agentsConfigurations[0].label}`}
render={({ field: { value, onChange } }) => (
<ConfigFieldAgents
id={agentsConfigurations[0].label}
label={agentsConfigurations[0].label}
agentId={agentId}
value={value}
setValue={onChange}
/>
)}
/>
</div>
</>
)}
</>
</div>
);
}
@@ -1,6 +1,7 @@
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
@@ -8,8 +9,8 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useAgents } from "@/hooks/use-agents";
import { Bot, LoaderCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { Bot, LoaderCircle, X } from "lucide-react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useAgentsContext } from "@/providers/Agents";
import { AgentFieldsForm, AgentFieldsFormLoading } from "./agent-form";
@@ -18,6 +19,7 @@ import { Agent } from "@/types/agent";
import { getDeployments } from "@/lib/environment/deployments";
import { GraphSelect } from "./graph-select";
import { useAgentConfig } from "@/hooks/use-agent-config";
import { FormProvider, useForm } from "react-hook-form";
interface CreateAgentDialogProps {
agentId?: string;
@@ -27,93 +29,51 @@ interface CreateAgentDialogProps {
onOpenChange: (open: boolean) => void;
}
export function CreateAgentDialog({
agentId,
deploymentId,
graphId,
open,
onOpenChange,
}: CreateAgentDialogProps) {
const deployments = getDeployments();
function CreateAgentFormContent(props: {
selectedGraph: Agent;
selectedDeployment: Deployment;
onClose: () => void;
}) {
const form = useForm<{
name: string;
description: string;
config: Record<string, any>;
}>({
defaultValues: async () => {
const values = await getSchemaAndUpdateConfig(props.selectedGraph);
return { name: "", description: "", config: values.config };
},
});
const { createAgent } = useAgents();
const { refreshAgents, agents } = useAgentsContext();
const { refreshAgents } = useAgentsContext();
const {
getSchemaAndUpdateConfig,
loading,
configurations,
toolConfigurations,
ragConfigurations,
agentsConfigurations,
config,
setConfig,
loading,
name,
setName,
description,
setDescription,
clearState: clearAgentConfigState,
} = useAgentConfig();
const [submitting, setSubmitting] = useState(false);
const [selectedDeployment, setSelectedDeployment] = useState<Deployment>();
// Use the default agent as the selected graph.
const [selectedGraph, setSelectedGraph] = useState<Agent>();
useEffect(() => {
if (selectedDeployment || selectedGraph) return;
if (agentId && deploymentId && graphId) {
// Find the deployment & default agent, then set them
const deployment = deployments.find((d) => d.id === deploymentId);
const defaultAgent = agents.find(
(a) => a.assistant_id === agentId && a.deploymentId === deploymentId,
);
if (!deployment || !defaultAgent) {
toast.error("Something went wrong. Please try again.", {
richColors: true,
});
return;
}
setSelectedDeployment(deployment);
setSelectedGraph(defaultAgent);
}
}, [agentId, deploymentId, graphId, agents, deployments]);
useEffect(() => {
if (
typeof window === "undefined" ||
loading ||
!open ||
!selectedGraph ||
!selectedDeployment
)
return;
getSchemaAndUpdateConfig(selectedGraph, {
isCreate: true,
});
}, [selectedGraph, selectedDeployment, open]);
const handleSubmit = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => {
e.preventDefault();
const handleSubmit = async (data: {
name: string;
description: string;
config: Record<string, any>;
}) => {
const { name, description, config } = data;
if (!name || !description) {
toast.warning("Name and description are required", {
richColors: true,
});
return;
}
if (!selectedGraph || !selectedDeployment) {
toast.error("Failed to create agent", {
description: "Please try again",
richColors: true,
});
return;
}
setSubmitting(true);
const newAgent = await createAgent(
selectedDeployment.id,
selectedGraph.graph_id,
props.selectedDeployment.id,
props.selectedGraph.graph_id,
{
name,
description,
@@ -134,37 +94,125 @@ export function CreateAgentDialog({
richColors: true,
});
onOpenChange(false);
clearState();
props.onClose();
// Do not await so that the refresh is non-blocking
refreshAgents();
};
const clearState = () => {
clearAgentConfigState();
setSelectedDeployment(undefined);
setSelectedGraph(undefined);
};
return (
<form onSubmit={form.handleSubmit(handleSubmit)}>
{loading ? (
<AgentFieldsFormLoading />
) : (
<FormProvider {...form}>
<AgentFieldsForm
agentId={props.selectedGraph.assistant_id}
configurations={configurations}
toolConfigurations={toolConfigurations}
ragConfigurations={ragConfigurations}
agentsConfigurations={agentsConfigurations}
/>
</FormProvider>
)}
<AlertDialogFooter>
<Button
onClick={(e) => {
e.preventDefault();
props.onClose();
}}
variant="outline"
disabled={loading || submitting}
>
Cancel
</Button>
<Button
type="submit"
className="flex w-full items-center justify-center gap-1"
disabled={loading || submitting}
>
{submitting ? <LoaderCircle className="animate-spin" /> : <Bot />}
<span>{submitting ? "Creating..." : "Create Agent"}</span>
</Button>
</AlertDialogFooter>
</form>
);
}
export function CreateAgentDialog({
agentId,
deploymentId,
graphId,
open,
onOpenChange,
}: CreateAgentDialogProps) {
const deployments = getDeployments();
const { agents } = useAgentsContext();
const [selectedDeployment, setSelectedDeployment] = useState<
Deployment | undefined
>();
const [selectedGraph, setSelectedGraph] = useState<Agent | undefined>();
useEffect(() => {
if (selectedDeployment || selectedGraph) return;
if (agentId && deploymentId && graphId) {
// Find the deployment & default agent, then set them
const deployment = deployments.find((d) => d.id === deploymentId);
const defaultAgent = agents.find(
(a) => a.assistant_id === agentId && a.deploymentId === deploymentId,
);
if (!deployment || !defaultAgent) {
toast.error("Something went wrong. Please try again.", {
richColors: true,
});
return;
}
setSelectedDeployment(deployment);
setSelectedGraph(defaultAgent);
}
}, [
agentId,
deploymentId,
graphId,
agents,
deployments,
selectedDeployment,
selectedGraph,
]);
const [openCounter, setOpenCounter] = useState(0);
const lastOpen = useRef(open);
useLayoutEffect(() => {
if (lastOpen.current !== open && open) {
setOpenCounter((c) => c + 1);
}
lastOpen.current = open;
}, [open, setOpenCounter]);
return (
<AlertDialog
open={open}
onOpenChange={(c) => {
onOpenChange(c);
if (!c) {
clearState();
}
}}
onOpenChange={onOpenChange}
>
<AlertDialogContent className="h-auto max-h-[90vh] overflow-auto sm:max-w-lg md:max-w-2xl lg:max-w-3xl">
<AlertDialogHeader>
<AlertDialogTitle>Create Agent</AlertDialogTitle>
<AlertDialogDescription>
Create a new agent for &apos;
<span className="font-medium">{selectedGraph?.graph_id}</span>&apos;
graph.
</AlertDialogDescription>
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-1.5">
<AlertDialogTitle>Create Agent</AlertDialogTitle>
<AlertDialogDescription>
Create a new agent for &apos;
<span className="font-medium">{selectedGraph?.graph_id}</span>
&apos; graph.
</AlertDialogDescription>
</div>
<AlertDialogCancel size="icon">
<X className="size-4" />
</AlertDialogCancel>
</div>
</AlertDialogHeader>
{!agentId && !graphId && !deploymentId && (
<div className="flex flex-col items-start justify-start gap-2">
<p>Please select a graph to create an agent for.</p>
@@ -178,44 +226,15 @@ export function CreateAgentDialog({
/>
</div>
)}
{loading ? (
<AgentFieldsFormLoading />
) : selectedGraph && selectedDeployment ? (
<AgentFieldsForm
name={name}
setName={setName}
description={description}
setDescription={setDescription}
configurations={configurations}
toolConfigurations={toolConfigurations}
config={config}
setConfig={setConfig}
agentId={selectedGraph.assistant_id}
ragConfigurations={ragConfigurations}
agentsConfigurations={agentsConfigurations}
{selectedGraph && selectedDeployment ? (
<CreateAgentFormContent
key={openCounter}
selectedGraph={selectedGraph}
selectedDeployment={selectedDeployment}
onClose={() => onOpenChange(false)}
/>
) : null}
<AlertDialogFooter>
<Button
onClick={(e) => {
e.preventDefault();
clearState();
onOpenChange(false);
}}
variant="outline"
disabled={loading || submitting}
>
Cancel
</Button>
<Button
onClick={(e) => handleSubmit(e)}
className="flex w-full items-center justify-center gap-1"
disabled={loading || submitting}
>
{submitting ? <LoaderCircle className="animate-spin" /> : <Bot />}
<span>{submitting ? "Creating..." : "Create Agent"}</span>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
@@ -11,11 +11,12 @@ import {
import { useAgents } from "@/hooks/use-agents";
import { useAgentConfig } from "@/hooks/use-agent-config";
import { Bot, LoaderCircle, Trash, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useAgentsContext } from "@/providers/Agents";
import { AgentFieldsForm, AgentFieldsFormLoading } from "./agent-form";
import { Agent } from "@/types/agent";
import { FormProvider, useForm } from "react-hook-form";
interface EditAgentDialogProps {
agent: Agent;
@@ -23,63 +24,47 @@ interface EditAgentDialogProps {
onOpenChange: (open: boolean) => void;
}
export function EditAgentDialog({
function EditAgentDialogContent({
agent,
open,
onOpenChange,
}: EditAgentDialogProps) {
onClose,
}: {
agent: Agent;
onClose: () => void;
}) {
const { updateAgent, deleteAgent } = useAgents();
const { refreshAgents } = useAgentsContext();
const {
getSchemaAndUpdateConfig,
loading,
configurations,
toolConfigurations,
ragConfigurations,
agentsConfigurations,
config,
setConfig,
loading,
setLoading,
name,
setName,
description,
setDescription,
clearState: clearAgentConfigState,
} = useAgentConfig();
const [submitting, setSubmitting] = useState(false);
const [deleteSubmitting, setDeleteSubmitting] = useState(false);
useEffect(() => {
if (
typeof window === "undefined" ||
loading ||
configurations.length > 0 ||
!open
)
return;
const form = useForm<{
name: string;
description: string;
config: Record<string, any>;
}>({ defaultValues: async () => getSchemaAndUpdateConfig(agent) });
getSchemaAndUpdateConfig(agent);
}, [agent, open]);
const handleSubmit = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => {
e.preventDefault();
if (!name || !description) {
const handleSubmit = async (data: {
name: string;
description: string;
config: Record<string, any>;
}) => {
if (!data.name || !data.description) {
toast.warning("Name and description are required");
return;
}
setSubmitting(true);
const updatedAgent = await updateAgent(
agent.assistant_id,
agent.deploymentId,
{
name,
description,
config,
},
data,
);
setSubmitting(false);
if (!updatedAgent) {
toast.error("Failed to update agent", {
@@ -90,16 +75,14 @@ export function EditAgentDialog({
toast.success("Agent updated successfully!");
onOpenChange(false);
clearState();
// Do not await so that the refresh is non-blocking
onClose();
refreshAgents();
};
const handleDelete = async () => {
setSubmitting(true);
setDeleteSubmitting(true);
const deleted = await deleteAgent(agent.deploymentId, agent.assistant_id);
setSubmitting(false);
setDeleteSubmitting(false);
if (!deleted) {
toast.error("Failed to delete agent", {
@@ -110,78 +93,100 @@ export function EditAgentDialog({
toast.success("Agent deleted successfully!");
onOpenChange(false);
clearState();
// Do not await so that the refresh is non-blocking
onClose();
refreshAgents();
};
const clearState = () => {
clearAgentConfigState();
setLoading(false);
setSubmitting(false);
};
return (
<AlertDialog
open={open}
onOpenChange={(c) => {
onOpenChange(c);
if (!c) {
clearState();
}
}}
>
<AlertDialogContent className="h-auto max-h-[90vh] overflow-auto sm:max-w-lg md:max-w-2xl lg:max-w-3xl">
<AlertDialogContent className="h-auto max-h-[90vh] overflow-auto sm:max-w-lg md:max-w-2xl lg:max-w-3xl">
<form onSubmit={form.handleSubmit(handleSubmit)}>
<AlertDialogHeader>
<div className="flex items-center justify-between">
<AlertDialogTitle>Edit Agent</AlertDialogTitle>
<AlertDialogCancel>
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-1.5">
<AlertDialogTitle>Edit Agent</AlertDialogTitle>
<AlertDialogDescription>
Edit the agent for &apos;
<span className="font-medium">{agent.graph_id}</span>&apos;
graph.
</AlertDialogDescription>
</div>
<AlertDialogCancel size="icon">
<X className="size-4" />
</AlertDialogCancel>
</div>
<AlertDialogDescription>
Edit the agent for &apos;
<span className="font-medium">{agent.graph_id}</span>&apos; graph.
</AlertDialogDescription>
</AlertDialogHeader>
{loading ? (
<AgentFieldsFormLoading />
) : (
<AgentFieldsForm
name={name}
setName={setName}
description={description}
setDescription={setDescription}
configurations={configurations}
toolConfigurations={toolConfigurations}
config={config}
setConfig={setConfig}
agentId={agent.assistant_id}
ragConfigurations={ragConfigurations}
agentsConfigurations={agentsConfigurations}
/>
<FormProvider {...form}>
<AgentFieldsForm
configurations={configurations}
toolConfigurations={toolConfigurations}
agentId={agent.assistant_id}
ragConfigurations={ragConfigurations}
agentsConfigurations={agentsConfigurations}
/>
</FormProvider>
)}
<AlertDialogFooter>
<Button
onClick={handleDelete}
className="flex w-full items-center justify-center gap-1"
disabled={loading || submitting}
disabled={loading || deleteSubmitting}
variant="destructive"
>
{submitting ? <LoaderCircle className="animate-spin" /> : <Trash />}
<span>{submitting ? "Deleting..." : "Delete Agent"}</span>
{deleteSubmitting ? (
<LoaderCircle className="animate-spin" />
) : (
<Trash />
)}
<span>{deleteSubmitting ? "Deleting..." : "Delete Agent"}</span>
</Button>
<Button
onClick={handleSubmit}
type="submit"
className="flex w-full items-center justify-center gap-1"
disabled={loading || submitting}
disabled={loading || form.formState.isSubmitting}
>
{submitting ? <LoaderCircle className="animate-spin" /> : <Bot />}
<span>{submitting ? "Saving..." : "Save Changes"}</span>
{form.formState.isSubmitting ? (
<LoaderCircle className="animate-spin" />
) : (
<Bot />
)}
<span>
{form.formState.isSubmitting ? "Saving..." : "Save Changes"}
</span>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</form>
</AlertDialogContent>
);
}
export function EditAgentDialog({
agent,
open,
onOpenChange,
}: EditAgentDialogProps) {
const [openCounter, setOpenCounter] = useState(0);
const lastOpen = useRef(open);
useLayoutEffect(() => {
if (lastOpen.current !== open && open) {
setOpenCounter((c) => c + 1);
}
lastOpen.current = open;
}, [open, setOpenCounter]);
return (
<AlertDialog
open={open}
onOpenChange={onOpenChange}
>
<EditAgentDialogContent
key={openCounter}
agent={agent}
onClose={() => onOpenChange(false)}
/>
</AlertDialog>
);
}
@@ -114,12 +114,11 @@ function NewThreadButton() {
export function Thread() {
const [agentId] = useQueryState("agentId");
const { getAgentConfig } = useConfigStore();
const [hideToolCalls, setHideToolCalls] = useQueryState(
"hideToolCalls",
parseAsBoolean.withDefault(false),
);
const [input, setInput] = useState("");
const [hasInput, setHasInput] = useState(false);
const [firstTokenReceived, setFirstTokenReceived] = useState(false);
const { session } = useAuthContext();
@@ -174,17 +173,26 @@ export function Thread() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const form = e.currentTarget as HTMLFormElement;
const formData = new FormData(form);
const content = (formData.get("input") as string | undefined)?.trim() ?? "";
setHasInput(false);
if (!content || isLoading) return;
if (!agentId) return;
setFirstTokenReceived(false);
const newHumanMessage: Message = {
id: uuidv4(),
type: "human",
content: input,
content,
};
const toolMessages = ensureToolCallsHaveResponses(stream.messages);
const { getAgentConfig } = useConfigStore.getState();
stream.submit(
{ messages: [...toolMessages, newHumanMessage] },
{
@@ -206,13 +214,14 @@ export function Thread() {
},
);
setInput("");
form.reset();
};
const handleRegenerate = (
parentCheckpoint: Checkpoint | null | undefined,
) => {
if (!agentId) return;
const { getAgentConfig } = useConfigStore.getState();
// Do this so the loading state is correct
prevMessageLength.current = prevMessageLength.current - 1;
@@ -296,8 +305,8 @@ export function Thread() {
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
name="input"
onChange={(e) => setHasInput(!!e.target.value.trim())}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
@@ -345,7 +354,7 @@ export function Thread() {
<Button
type="submit"
className="shadow-md transition-all"
disabled={isLoading || !input.trim()}
disabled={isLoading || !hasInput}
>
Send
</Button>
@@ -43,9 +43,7 @@ export function HumanMessage({
isLoading: boolean;
}) {
const { session } = useAuthContext();
const [agentId] = useQueryState("agentId");
const { getAgentConfig } = useConfigStore();
const thread = useStreamContext();
const meta = thread.getMessagesMetadata(message);
@@ -61,6 +59,8 @@ export function HumanMessage({
setIsEditing(false);
const newMessage: Message = { type: "human", content: value };
const { getAgentConfig } = useConfigStore.getState();
thread.submit(
{ messages: [newMessage] },
{
+77 -133
View File
@@ -4,13 +4,9 @@ import {
ConfigurableFieldRAGMetadata,
ConfigurableFieldUIMetadata,
} from "@/types/configurable";
import { useState } from "react";
import { useCallback, useState } from "react";
import { useAgents } from "./use-agents";
import {
configSchemaToAgentsConfig,
configSchemaToConfigurableFields,
configSchemaToConfigurableTools,
configSchemaToRagConfig,
extractConfigurationsFromAgent,
getConfigurableDefaults,
} from "@/lib/ui-config";
@@ -38,155 +34,103 @@ export function useAgentConfig() {
>([]);
const [supportedConfigs, setSupportedConfigs] = useState<string[]>([]);
// The raw configurable fields. Only contains key value pairs, and nothing
// around the UI config.
const [config, setConfig] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const clearState = () => {
const clearState = useCallback(() => {
setConfigurations([]);
setToolConfigurations([]);
setRagConfigurations([]);
setAgentsConfigurations([]);
setConfig({});
setName("");
setDescription("");
setLoading(false);
};
}, []);
const getSchemaAndUpdateConfig = async (
agent: Agent,
args?: {
isCreate?: boolean;
},
) => {
clearState();
const getSchemaAndUpdateConfig = useCallback(
async (
agent: Agent,
): Promise<{
name: string;
description: string;
config: Record<string, any>;
}> => {
clearState();
setLoading(true);
try {
const schema = await getAgentConfigSchema(
agent.assistant_id,
agent.deploymentId,
);
if (!schema) return;
const { configFields, toolConfig, ragConfig, agentsConfig } =
extractConfigurationsFromAgent({
agent,
schema,
});
setLoading(true);
try {
const schema = await getAgentConfigSchema(
agent.assistant_id,
agent.deploymentId,
);
if (!schema)
return {
name: agent.name,
description:
(agent.metadata?.description as string | undefined) ?? "",
config: {},
};
const { configFields, toolConfig, ragConfig, agentsConfig } =
extractConfigurationsFromAgent({
agent,
schema,
});
const agentId = agent.assistant_id;
const agentId = agent.assistant_id;
setConfigurations(configFields);
setToolConfigurations(toolConfig);
// Set default config values based on configuration fields
const { setDefaultConfig } = useConfigStore.getState();
setDefaultConfig(agentId, configFields);
const supportedConfigs: string[] = [];
if (toolConfig.length) {
setDefaultConfig(`${agentId}:selected-tools`, toolConfig);
setConfigurations(configFields);
setToolConfigurations(toolConfig);
supportedConfigs.push("tools");
// Set default config values based on configuration fields
const { setDefaultConfig } = useConfigStore.getState();
setDefaultConfig(agentId, configFields);
const supportedConfigs: string[] = [];
if (toolConfig.length) {
setDefaultConfig(`${agentId}:selected-tools`, toolConfig);
setToolConfigurations(toolConfig);
supportedConfigs.push("tools");
}
if (ragConfig.length) {
setDefaultConfig(`${agentId}:rag`, ragConfig);
setRagConfigurations(ragConfig);
supportedConfigs.push("rag");
}
if (agentsConfig.length) {
setDefaultConfig(`${agentId}:agents`, agentsConfig);
setAgentsConfigurations(agentsConfig);
supportedConfigs.push("supervisor");
}
setSupportedConfigs(supportedConfigs);
const configurableDefaults = getConfigurableDefaults(
configFields,
toolConfig,
ragConfig,
agentsConfig,
);
return {
name: agent.name,
description:
(agent.metadata?.description as string | undefined) ?? "",
config: configurableDefaults,
};
} finally {
setLoading(false);
}
if (ragConfig.length) {
setDefaultConfig(`${agentId}:rag`, ragConfig);
setRagConfigurations(ragConfig);
supportedConfigs.push("rag");
}
if (agentsConfig.length) {
setDefaultConfig(`${agentId}:agents`, agentsConfig);
setAgentsConfigurations(agentsConfig);
supportedConfigs.push("supervisor");
}
if (!args?.isCreate) {
// Don't set name/description for create agents, since these are user specified fields.
setName(agent.name);
setDescription((agent.metadata?.description ?? "") as string);
}
const configurableDefaults = getConfigurableDefaults(
configFields,
toolConfig,
ragConfig,
agentsConfig,
);
setConfig(configurableDefaults);
setSupportedConfigs(supportedConfigs);
} finally {
setLoading(false);
}
};
const resetToDefaultConfig = async (agent: Agent) => {
const schema = await getAgentConfigSchema(
agent.assistant_id,
agent.deploymentId,
);
if (!schema) return;
const agentId = agent.assistant_id;
const configFields = configSchemaToConfigurableFields(schema);
const toolConfig = configSchemaToConfigurableTools(schema);
const ragConfig = configSchemaToRagConfig(schema);
const agentsConfig = configSchemaToAgentsConfig(schema);
const { setDefaultConfig } = useConfigStore.getState();
setDefaultConfig(agentId, configFields);
const supportedConfigs: string[] = [];
if (toolConfig.length) {
setDefaultConfig(`${agentId}:selected-tools`, toolConfig);
setToolConfigurations(toolConfig);
supportedConfigs.push("tools");
}
if (ragConfig) {
setDefaultConfig(`${agentId}:rag`, [ragConfig]);
setRagConfigurations([ragConfig]);
supportedConfigs.push("rag");
}
if (agentsConfig) {
setDefaultConfig(`${agentId}:agents`, [agentsConfig]);
setAgentsConfigurations([agentsConfig]);
}
const configurableDefaults = getConfigurableDefaults(
configFields,
toolConfig,
ragConfig ? [ragConfig] : [],
agentsConfig ? [agentsConfig] : [],
);
setConfig(configurableDefaults);
};
},
[clearState, getAgentConfigSchema],
);
return {
clearState,
resetToDefaultConfig,
getSchemaAndUpdateConfig,
configurations,
setConfigurations,
toolConfigurations,
setToolConfigurations,
ragConfigurations,
setRagConfigurations,
agentsConfigurations,
setAgentsConfigurations,
config,
setConfig,
loading,
setLoading,
name,
setName,
description,
setDescription,
supportedConfigs,
setSupportedConfigs,
loading,
};
}
+151 -144
View File
@@ -3,158 +3,165 @@ import { Agent } from "@/types/agent";
import { Assistant } from "@langchain/langgraph-sdk";
import { toast } from "sonner";
import { useAuthContext } from "@/providers/Auth";
import { useCallback } from "react";
export function useAgents() {
const { session } = useAuthContext();
const getAgent = async (
agentId: string,
deploymentId: string,
): Promise<Agent | undefined> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const agent = await client.assistants.get(agentId);
return {
...agent,
deploymentId,
};
} catch (e) {
console.error("Failed to get agent", e);
toast.error("Failed to get agent");
return undefined;
}
};
const getAgentConfigSchema = async (
agentId: string,
deploymentId: string,
) => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const schemas = await client.assistants.getSchemas(agentId);
return schemas.config_schema ?? undefined;
} catch (e) {
console.error("Failed to get agent config schema", e);
toast.error("Failed to get agent config schema", {
description: (
<div className="flex flex-col items-start gap-2">
<p>
Agent ID:{" "}
<span className="font-mono font-semibold">{agentId}</span>
</p>
<p>
Deployment ID:{" "}
<span className="font-mono font-semibold">{deploymentId}</span>
</p>
</div>
),
richColors: true,
});
}
};
const createAgent = async (
deploymentId: string,
graphId: string,
args: {
name: string;
description: string;
config: Record<string, any>;
const getAgent = useCallback(
async (
agentId: string,
deploymentId: string,
): Promise<Agent | undefined> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const agent = await client.assistants.get(agentId);
return { ...agent, deploymentId };
} catch (e) {
console.error("Failed to get agent", e);
toast.error("Failed to get agent");
return undefined;
}
},
): Promise<Assistant | undefined> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const agent = await client.assistants.create({
graphId,
metadata: {
description: args.description,
},
name: args.name,
config: {
configurable: {
...args.config,
[session?.accessToken],
);
const getAgentConfigSchema = useCallback(
async (agentId: string, deploymentId: string) => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const schemas = await client.assistants.getSchemas(agentId);
return schemas.config_schema ?? undefined;
} catch (e) {
console.error("Failed to get agent config schema", e);
toast.error("Failed to get agent config schema", {
description: (
<div className="flex flex-col items-start gap-2">
<p>
Agent ID:{" "}
<span className="font-mono font-semibold">{agentId}</span>
</p>
<p>
Deployment ID:{" "}
<span className="font-mono font-semibold">{deploymentId}</span>
</p>
</div>
),
richColors: true,
});
}
},
[session?.accessToken],
);
const createAgent = useCallback(
async (
deploymentId: string,
graphId: string,
args: {
name: string;
description: string;
config: Record<string, any>;
},
): Promise<Assistant | undefined> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const agent = await client.assistants.create({
graphId,
metadata: {
description: args.description,
},
},
});
return agent;
} catch (e) {
console.error("Failed to create agent", e);
toast.error("Failed to create agent");
return undefined;
}
};
const updateAgent = async (
agentId: string,
deploymentId: string,
args: {
name?: string;
description?: string;
config?: Record<string, any>;
name: args.name,
config: {
configurable: {
...args.config,
},
},
});
return agent;
} catch (e) {
console.error("Failed to create agent", e);
toast.error("Failed to create agent");
return undefined;
}
},
): Promise<Assistant | undefined> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const agent = await client.assistants.update(agentId, {
metadata: {
...(args.description && { description: args.description }),
},
...(args.name && { name: args.name }),
...(args.config && { config: { configurable: args.config } }),
});
return agent;
} catch (e) {
console.error("Failed to update agent", e);
toast.error("Failed to update agent");
return undefined;
}
};
[session?.accessToken],
);
const deleteAgent = async (
deploymentId: string,
agentId: string,
): Promise<boolean> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return false;
}
try {
const client = createClient(deploymentId, session.accessToken);
await client.assistants.delete(agentId);
return true;
} catch (e) {
console.error("Failed to delete agent", e);
toast.error("Failed to delete agent");
return false;
}
};
const updateAgent = useCallback(
async (
agentId: string,
deploymentId: string,
args: {
name?: string;
description?: string;
config?: Record<string, any>;
},
): Promise<Assistant | undefined> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return;
}
try {
const client = createClient(deploymentId, session.accessToken);
const agent = await client.assistants.update(agentId, {
metadata: {
...(args.description && { description: args.description }),
},
...(args.name && { name: args.name }),
...(args.config && { config: { configurable: args.config } }),
});
return agent;
} catch (e) {
console.error("Failed to update agent", e);
toast.error("Failed to update agent");
return undefined;
}
},
[session?.accessToken],
);
const deleteAgent = useCallback(
async (deploymentId: string, agentId: string): Promise<boolean> => {
if (!session?.accessToken) {
toast.error("No access token found", {
richColors: true,
});
return false;
}
try {
const client = createClient(deploymentId, session.accessToken);
await client.assistants.delete(agentId);
return true;
} catch (e) {
console.error("Failed to delete agent", e);
toast.error("Failed to delete agent");
return false;
}
},
[session?.accessToken],
);
return {
getAgent,
+4 -1
View File
@@ -17,5 +17,8 @@
"turbo": "^2.5.0",
"typescript": "^5"
},
"packageManager": "yarn@3.5.1"
"packageManager": "yarn@3.5.1",
"dependencies": {
"react-hook-form": "^7.56.3"
}
}
+10
View File
@@ -6419,6 +6419,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "open-agent-platform@workspace:."
dependencies:
react-hook-form: ^7.56.3
turbo: ^2.5.0
typescript: ^5
languageName: unknown
@@ -6834,6 +6835,15 @@ __metadata:
languageName: node
linkType: hard
"react-hook-form@npm:^7.56.3":
version: 7.56.3
resolution: "react-hook-form@npm:7.56.3"
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
checksum: 5b5ebf1b61754a5c2e5785a6f3039eac89028d0613e9f7da23c132129e9685c166077b338db05430292d16b9532a21ebd2abd8785de497c54cb70fbccf774572
languageName: node
linkType: hard
"react-is@npm:^16.13.1":
version: 16.13.1
resolution: "react-is@npm:16.13.1"